using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; public class NativeAlarmProjectorTests { [Fact] public void Raise_projects_active_unacked_with_constant_fields_and_mapped_severity() { var sut = new NativeAlarmProjector(); var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise, AlarmSeverity.High, "boom")); snap.Active.ShouldBeTrue(); snap.Acknowledged.ShouldBeFalse(); snap.Confirmed.ShouldBeTrue(); snap.Enabled.ShouldBeTrue(); snap.Shelving.ShouldBe(AlarmShelvingKind.Unshelved); snap.Severity.ShouldBe((ushort)700); snap.Message.ShouldBe("boom"); } [Fact] public void Retrigger_projects_active_unacked() { var sut = new NativeAlarmProjector(); var snap = sut.Project("n1", Evt(AlarmTransitionKind.Retrigger)); snap.Active.ShouldBeTrue(); snap.Acknowledged.ShouldBeFalse(); } [Fact] public void Acknowledge_after_raise_acks_and_keeps_active() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); var snap = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge)); snap.Active.ShouldBeTrue(); snap.Acknowledged.ShouldBeTrue(); } [Fact] public void Clear_after_raise_deactivates_and_keeps_prior_ack() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // active, unacked var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear)); snap.Active.ShouldBeFalse(); snap.Acknowledged.ShouldBeFalse(); // preserved prior (unacked) ack state } [Fact] public void Clear_after_acknowledge_deactivates_and_keeps_acked() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge)); // active, acked var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear)); snap.Active.ShouldBeFalse(); snap.Acknowledged.ShouldBeTrue(); // preserved prior (acked) ack state } [Fact] public void Unspecified_carries_prior_active_and_ack_state() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // active, unacked var snap = sut.Project("n1", Evt(AlarmTransitionKind.Unspecified)); snap.Active.ShouldBeTrue(); snap.Acknowledged.ShouldBeFalse(); } [Fact] public void Cold_state_defaults_to_inactive_acked() { var sut = new NativeAlarmProjector(); // Acknowledge on a never-seen node: prior Active defaults false, ack set true. var snap = sut.Project("nNew", Evt(AlarmTransitionKind.Acknowledge)); snap.Active.ShouldBeFalse(); snap.Acknowledged.ShouldBeTrue(); } [Fact] public void Per_node_state_is_isolated() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // n1 active // n2 is cold; a Clear on it must stay inactive and not borrow n1's active state. var n2 = sut.Project("n2", Evt(AlarmTransitionKind.Clear)); n2.Active.ShouldBeFalse(); // n1 is unaffected by the n2 transition. var n1 = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge)); n1.Active.ShouldBeTrue(); } [Theory] [InlineData(AlarmSeverity.Low, (ushort)200)] [InlineData(AlarmSeverity.Medium, (ushort)500)] [InlineData(AlarmSeverity.High, (ushort)700)] [InlineData(AlarmSeverity.Critical, (ushort)900)] public void Severity_buckets_map_to_opcua_scale(AlarmSeverity sev, ushort expected) { var sut = new NativeAlarmProjector(); var snap = sut.Project("n1", Evt(AlarmTransitionKind.Raise, sev)); snap.Severity.ShouldBe(expected); } [Fact] public void Severity_and_message_always_come_from_the_event() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise, AlarmSeverity.High, "first")); // A later Clear carries its own severity + message; the snapshot reflects the event, not prior. var snap = sut.Project("n1", Evt(AlarmTransitionKind.Clear, AlarmSeverity.Low, "second")); snap.Severity.ShouldBe((ushort)200); snap.Message.ShouldBe("second"); } [Fact] public void Clear_resets_tracked_state() { var sut = new NativeAlarmProjector(); sut.Project("n1", Evt(AlarmTransitionKind.Raise)); // n1 active, unacked sut.Clear(); // After Clear() the node is cold again: an Acknowledge sees prior Active=false. var snap = sut.Project("n1", Evt(AlarmTransitionKind.Acknowledge)); snap.Active.ShouldBeFalse(); snap.Acknowledged.ShouldBeTrue(); } private static AlarmEventArgs Evt( AlarmTransitionKind kind, AlarmSeverity sev = AlarmSeverity.High, string msg = "m") => new(new H(), "Tank1.Hi", "c1", "LimitAlarm.Hi", msg, sev, DateTime.UnixEpoch, Kind: kind); private sealed class H : IAlarmSubscriptionHandle { public string DiagnosticId => "t"; } }