using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.SiteRuntime; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Actors; /// /// Task 15: NativeAlarmActor mirrors a source's native alarms — subscribe on start, /// emit enriched AlarmStateChanged, snapshot atomic swap, retention, persistence. /// public class NativeAlarmActorTests : TestKit, IDisposable { private readonly string _dbFile; private readonly SiteStorageService _storage; private readonly SiteRuntimeOptions _options = new(); public NativeAlarmActorTests() { _dbFile = Path.Combine(Path.GetTempPath(), $"naa-{Guid.NewGuid():N}.db"); _storage = new SiteStorageService($"Data Source={_dbFile}", NullLogger.Instance); _storage.InitializeAsync().GetAwaiter().GetResult(); } private static ResolvedNativeAlarmSource Source() => new() { CanonicalName = "Pressure", ConnectionName = "Opc", SourceReference = "ns=2;s=T01" }; private static NativeAlarmTransition Transition( string sourceRef, AlarmTransitionKind kind, AlarmConditionState condition, DateTimeOffset? time = null) => new(sourceRef, "T01", "AnalogLimit.Hi", kind, condition, "Process", "hi", "hi", "", "", null, time ?? DateTimeOffset.UtcNow, "92", "90"); private IActorRef Spawn(IActorRef instanceActor, IActorRef dclManager) => ActorOf(Props.Create(() => new NativeAlarmActor( Source(), "inst", instanceActor, dclManager, _storage, _options, NullLogger.Instance))); [Fact] public void SubscribeOnStart_SendsRequestForSourceBinding() { var dcl = CreateTestProbe(); Spawn(CreateTestProbe().Ref, dcl.Ref); var req = dcl.ExpectMsg(); Assert.Equal("inst", req.InstanceUniqueName); Assert.Equal("Opc", req.ConnectionName); Assert.Equal("ns=2;s=T01", req.SourceReference); } [Fact] public void Raise_EmitsEnrichedAlarmStateChanged() { var instance = CreateTestProbe(); var dcl = CreateTestProbe(); var actor = Spawn(instance.Ref, dcl.Ref); dcl.ExpectMsg(); actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( "T01.Hi", AlarmTransitionKind.Raise, new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)))); var emitted = instance.ExpectMsg(); Assert.Equal(AlarmKind.NativeOpcUa, emitted.Kind); Assert.Equal("T01.Hi", emitted.SourceReference); Assert.Equal(AlarmState.Active, emitted.State); Assert.Equal(800, emitted.Condition.Severity); } [Fact] public void SnapshotComplete_WithMissingPriorAlarm_EmitsReturnToNormal() { var instance = CreateTestProbe(); var dcl = CreateTestProbe(); var actor = Spawn(instance.Ref, dcl.Ref); dcl.ExpectMsg(); actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( "T01.Hi", AlarmTransitionKind.Raise, new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)))); instance.ExpectMsg(m => m.State == AlarmState.Active); // A fresh snapshot that no longer contains T01.Hi means it cleared at the source. actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( "T01.Hi", AlarmTransitionKind.SnapshotComplete, new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800)))); var cleared = instance.ExpectMsg(); Assert.Equal("T01.Hi", cleared.SourceReference); Assert.Equal(AlarmState.Normal, cleared.State); } [Fact] public void OlderTransition_IsIgnored() { var instance = CreateTestProbe(); var dcl = CreateTestProbe(); var actor = Spawn(instance.Ref, dcl.Ref); dcl.ExpectMsg(); var t0 = DateTimeOffset.UtcNow; actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( "T01.Hi", AlarmTransitionKind.Raise, new AlarmConditionState(true, false, null, AlarmShelveState.Unshelved, false, 800), t0))); instance.ExpectMsg(m => m.State == AlarmState.Active); // An out-of-order (older) transition must not overwrite newer state. actor.Tell(new NativeAlarmTransitionUpdate("Opc", Transition( "T01.Hi", AlarmTransitionKind.Clear, new AlarmConditionState(false, true, null, AlarmShelveState.Unshelved, false, 0), t0.AddSeconds(-30)))); instance.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); } void IDisposable.Dispose() { Shutdown(); if (File.Exists(_dbFile)) { File.Delete(_dbFile); } } }