using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// 05/STAB-9 — the poll engine's onError sink must be wired to the driver health /// surface: a poll-loop reader failure degrades health while preserving /// , and must never downgrade a pre-existing /// . /// [Trait("Category", "Unit")] public sealed class FocasPollErrorHealthTests { /// A poll reader failure degrades health, preserving the last successful read timestamp. [Fact] public async Task PollReaderFailure_DegradesHealth_PreservesLastSuccessfulRead() { var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Probe = new FocasProbeOptions { Enabled = false }, }, "focas-1", new FakeFocasClientFactory()); await drv.InitializeAsync("{}", CancellationToken.None); var before = drv.GetHealth(); before.State.ShouldBe(DriverState.Healthy); before.LastSuccessfulRead.ShouldNotBeNull(); drv.HandlePollError(new InvalidOperationException("poll boom")); var after = drv.GetHealth(); after.State.ShouldBe(DriverState.Degraded); after.LastSuccessfulRead.ShouldBe(before.LastSuccessfulRead); after.LastError.ShouldNotBeNull(); after.LastError.ShouldContain("poll boom"); } /// A poll reader failure never downgrades a pre-existing Faulted state. [Fact] public async Task PollReaderFailure_NeverDowngradesFaulted() { var drv = new FocasDriver(new FocasDriverOptions { Devices = [new FocasDeviceOptions("focas://10.0.0.5:8193")], Tags = [new FocasTagDefinition("X", "focas://10.0.0.5:9999", "R100", FocasDataType.Byte)], Probe = new FocasProbeOptions { Enabled = false }, }, "focas-1", new FakeFocasClientFactory()); await Should.ThrowAsync(() => drv.InitializeAsync("{}", CancellationToken.None)); drv.GetHealth().State.ShouldBe(DriverState.Faulted); drv.HandlePollError(new InvalidOperationException("poll boom")); drv.GetHealth().State.ShouldBe(DriverState.Faulted); } }