using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using ArchestrA.MxAccess; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.MxAccess; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests; [Trait("Category", "Unit")] public sealed class HostStatusPushTests { /// /// PR 8 — when MxAccessClient.ConnectionStateChanged fires false→true→false, /// MxAccessGalaxyBackend raises OnHostStatusChanged once per transition with /// HostName=ClientName, RuntimeStatus="Running"/"Stopped", and a timestamp. /// This is the gateway-level signal; per-platform ScanState probes are deferred. /// [Fact] public async Task ConnectionStateChanged_raises_OnHostStatusChanged_with_gateway_name() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var proxy = new FakeProxy(); var mx = new MxAccessClient(pump, proxy, "GatewayClient", new MxAccessClientOptions { AutoReconnect = false }); using var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, historian: null); var notifications = new ConcurrentQueue(); backend.OnHostStatusChanged += (_, s) => notifications.Enqueue(s); await mx.ConnectAsync(); await mx.DisconnectAsync(); notifications.Count.ShouldBe(2); notifications.TryDequeue(out var first).ShouldBeTrue(); first!.HostName.ShouldBe("GatewayClient"); first.RuntimeStatus.ShouldBe("Running"); first.LastObservedUtcUnixMs.ShouldBeGreaterThan(0); notifications.TryDequeue(out var second).ShouldBeTrue(); second!.HostName.ShouldBe("GatewayClient"); second.RuntimeStatus.ShouldBe("Stopped"); } [Fact] public async Task Dispose_unsubscribes_so_post_dispose_state_changes_do_not_fire_events() { using var pump = new StaPump("Test.Sta"); await pump.WaitForStartedAsync(); var proxy = new FakeProxy(); var mx = new MxAccessClient(pump, proxy, "GatewayClient", new MxAccessClientOptions { AutoReconnect = false }); var backend = new MxAccessGalaxyBackend( new GalaxyRepository(new GalaxyRepositoryOptions { ConnectionString = "Server=.;Database=ZB;Integrated Security=True;" }), mx, historian: null); var count = 0; backend.OnHostStatusChanged += (_, _) => Interlocked.Increment(ref count); await mx.ConnectAsync(); count.ShouldBe(1); backend.Dispose(); await mx.DisconnectAsync(); count.ShouldBe(1); // no second notification after Dispose } private sealed class FakeProxy : IMxProxy { private int _next = 1; public int Register(string _) => 42; public void Unregister(int _) { } public int AddItem(int _, string __) => Interlocked.Increment(ref _next); public void RemoveItem(int _, int __) { } public void AdviseSupervisory(int _, int __) { } public void UnAdviseSupervisory(int _, int __) { } public void Write(int _, int __, object ___, int ____) { } public event MxDataChangeHandler? OnDataChange { add { } remove { } } public event MxWriteCompleteHandler? OnWriteComplete { add { } remove { } } } }