Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Browse/GatewayGalaxyPlatformSourceTests.cs
T
Joseph Doherty 3456756cd1 fix(galaxy): seed the probe set at init, refuse pumps mid-teardown, bound the close
Review fix round 1 for lane F:
- The ScanState probe watcher is seeded at InitializeAsync from a platform-only
  DiscoverHierarchy query (IGalaxyPlatformSource / GatewayGalaxyPlatformSource over the owned
  repository client) and re-seeded on every deploy-watcher rediscovery. The runtime never
  calls DiscoverAsync on a live driver, so per-platform host status was unreachable in
  production. A failed fetch logs a Warning and never fails init; re-seeds coalesce so a
  deploy during an in-flight pass still gets a pass that starts after it.
- EnsureEventPumpStarted refuses while the runtime is stopping, BEFORE reusing the live pump
  (that pump belongs to the dying subscriber), and the production rebuild disposes any stale
  pump before wiring the new subscriber.
- TeardownRuntimeAsync closes the MX session under the caller's token linked with a 5 s
  SessionCloseDeadline, so a blackholed gateway cannot hold DriverInstanceActor.PostStop for 30 s.
- A runtime generation counter, bumped by every teardown, is re-checked before each Rebind
  in ResubscribeAllAsync, so a replay-gap re-snapshot that straddles a teardown cannot
  resurrect handles into the cleared registry.

Refs #735

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2026-10-22 13:29:11 -04:00

61 lines
2.5 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Browse;
/// <summary>The platform-only query the driver seeds its ScanState probe watcher from.</summary>
public sealed class GatewayGalaxyPlatformSourceTests
{
private static GalaxyObject Obj(string tag, params string[] chain)
{
var o = new GalaxyObject { TagName = tag };
o.TemplateChain.Add(chain);
return o;
}
/// <summary>Every request carries the platform template filter, no attributes, and the page size; replies are re-filtered.</summary>
[Fact]
public async Task Requests_filter_on_platform_templates_without_attributes_and_replies_are_refiltered()
{
var requests = new List<DiscoverHierarchyRequest>();
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
{
requests.Add(req);
var reply = new DiscoverHierarchyReply();
if (req.PageToken == "")
{
reply.Objects.Add([Obj("Plat01", "$WinPlatform"), Obj("Tank01", "$UserDefined")]);
reply.NextPageToken = "p2";
}
else
{
reply.Objects.Add([Obj("Eng01", "$AppEngine", "$WinPlatform"), Obj("Plat01", "$WinPlatform"), Obj("", "$WinPlatform")]);
}
return Task.FromResult(reply);
}
var names = await GatewayGalaxyPlatformSource.QueryAsync(Discover, pageSize: 50, CancellationToken.None);
names.ShouldBe(["Plat01", "Eng01"]);
requests.Count.ShouldBe(2);
requests.ShouldAllBe(r => r.PageSize == 50 && r.IncludeAttributes == false);
requests.ShouldAllBe(r => r.TemplateChainContains.Contains("$WinPlatform") && r.TemplateChainContains.Contains("$AppEngine"));
}
/// <summary>A gateway that strips the template chain is trusted to have filtered server-side.</summary>
[Fact]
public async Task Objects_without_a_template_chain_are_kept()
{
Task<DiscoverHierarchyReply> Discover(DiscoverHierarchyRequest req, CancellationToken ct)
{
var reply = new DiscoverHierarchyReply();
reply.Objects.Add(new GalaxyObject { TagName = "Plat01" });
return Task.FromResult(reply);
}
(await GatewayGalaxyPlatformSource.QueryAsync(Discover, pageSize: 0, CancellationToken.None)).ShouldBe(["Plat01"]);
}
}