using System; using System.Collections.Generic; using MxGateway.Contracts.Proto; using MxGateway.Worker.MxAccess; namespace MxGateway.Worker.Tests.MxAccess; /// /// Unit tests for the in-process A.3 dispatcher: prove that /// events /// fan out to the worker's as proto /// messages with correctly mapped /// fields. The fake consumer below stands in for the wnwrap-backed /// production implementation so this exercise needs no AVEVA install. /// public sealed class AlarmDispatcherTests { private const string SessionId = "session-001"; [Fact] public void OnTransition_WhenAlarmTransitionRaised_LandsInQueueWithMappedFields() { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); MxAccessEventQueue queue = new MxAccessEventQueue(); MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc); consumer.RaiseTransition(new MxAlarmTransitionEvent { PreviousState = MxAlarmStateKind.Unspecified, Record = new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "TestArea", TagName = "TestMachine_001.TestAlarm001", Type = "DSC", Priority = 500, State = MxAlarmStateKind.UnackAlm, TransitionTimestampUtc = ts, AlarmComment = "Test alarm #1", }, }); Assert.Equal(1, queue.Count); Assert.True(queue.TryDequeue(out WorkerEvent? workerEvent)); Assert.NotNull(workerEvent); MxEvent mxEvent = workerEvent!.Event; Assert.Equal(MxEventFamily.OnAlarmTransition, mxEvent.Family); Assert.Equal(SessionId, mxEvent.SessionId); OnAlarmTransitionEvent body = mxEvent.OnAlarmTransition; Assert.NotNull(body); Assert.Equal("Galaxy!TestArea.TestMachine_001.TestAlarm001", body.AlarmFullReference); Assert.Equal("TestMachine_001.TestAlarm001", body.SourceObjectReference); Assert.Equal("DSC", body.AlarmTypeName); Assert.Equal(AlarmTransitionKind.Raise, body.TransitionKind); Assert.Equal(500, body.Severity); Assert.Equal("Test alarm #1", body.OperatorComment); Assert.Equal("TestArea", body.Category); Assert.NotNull(body.TransitionTimestamp); Assert.Equal(ts, body.TransitionTimestamp.ToDateTime()); } [Fact] public void OnTransition_WithConsecutiveUnchangedState_DoesNotEmitTransition() { // Mapper.MapTransition returns Unspecified when the state didn't // change; the dispatcher should drop the event before queueing. FakeAlarmConsumer consumer = new FakeAlarmConsumer(); MxAccessEventQueue queue = new MxAccessEventQueue(); MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); consumer.RaiseTransition(new MxAlarmTransitionEvent { PreviousState = MxAlarmStateKind.UnackAlm, Record = new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "X", TagName = "Y", State = MxAlarmStateKind.UnackAlm, }, }); Assert.Equal(0, queue.Count); } [Theory] [InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Acknowledge)] [InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.UnackRtn, AlarmTransitionKind.Clear)] [InlineData(MxAlarmStateKind.UnackRtn, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)] public void MapTransition_ForEachStatePair_FollowsStateTable( MxAlarmStateKind previous, MxAlarmStateKind current, AlarmTransitionKind expected) { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); MxAccessEventQueue queue = new MxAccessEventQueue(); MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); consumer.RaiseTransition(new MxAlarmTransitionEvent { PreviousState = previous, Record = new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "G", TagName = "T", State = current, }, }); Assert.Equal(1, queue.Count); queue.TryDequeue(out WorkerEvent? evt); Assert.Equal(expected, evt!.Event.OnAlarmTransition.TransitionKind); } [Fact] public void Subscribe_WhenInvoked_ForwardsToConsumer() { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); using AlarmDispatcher dispatcher = new AlarmDispatcher( consumer, new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), SessionId); dispatcher.Subscribe(@"\\HOST\Galaxy!Area1"); Assert.Equal(@"\\HOST\Galaxy!Area1", consumer.LastSubscription); } [Fact] public void Acknowledge_WhenInvoked_ForwardsToConsumerWithFullOperatorIdentity() { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); consumer.AcknowledgeReturn = 0; using AlarmDispatcher dispatcher = new AlarmDispatcher( consumer, new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), SessionId); Guid guid = Guid.NewGuid(); int rc = dispatcher.Acknowledge( guid, "Acked", "alice", "WS01", "CORP", "Alice Smith"); Assert.Equal(0, rc); Assert.Equal(guid, consumer.LastAckGuid); Assert.Equal("Acked", consumer.LastAckComment); Assert.Equal("alice", consumer.LastAckOperatorName); Assert.Equal("WS01", consumer.LastAckOperatorNode); Assert.Equal("CORP", consumer.LastAckOperatorDomain); Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName); } [Fact] public void AcknowledgeByName_WhenInvoked_ForwardsToConsumerWithFullTuple() { FakeAlarmConsumer consumer = new FakeAlarmConsumer { AcknowledgeReturn = 0 }; using AlarmDispatcher dispatcher = new AlarmDispatcher( consumer, new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), SessionId); int rc = dispatcher.AcknowledgeByName( alarmName: "TestMachine_001.TestAlarm001", providerName: "Galaxy", groupName: "TestArea", ackComment: "ack", ackOperatorName: "alice", ackOperatorNode: "WS", ackOperatorDomain: "CORP", ackOperatorFullName: "Alice Smith"); Assert.Equal(0, rc); Assert.NotNull(consumer.LastAckByNameTuple); Assert.Equal("TestMachine_001.TestAlarm001", consumer.LastAckByNameTuple!.Value.Name); Assert.Equal("Galaxy", consumer.LastAckByNameTuple!.Value.Provider); Assert.Equal("TestArea", consumer.LastAckByNameTuple!.Value.Group); } [Fact] public void SnapshotActiveAlarms_WhenConsumerHasRecords_MapsRecordsToProtos() { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); DateTime ts = new DateTime(2026, 5, 1, 17, 26, 14, 709, DateTimeKind.Utc); consumer.SnapshotResult = new[] { new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "TestArea", TagName = "Tag1", Type = "DSC", Priority = 500, State = MxAlarmStateKind.UnackAlm, TransitionTimestampUtc = ts, AlarmComment = "x", }, new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "TestArea", TagName = "Tag2", Type = "ANL", Priority = 100, State = MxAlarmStateKind.AckAlm, TransitionTimestampUtc = ts, }, }; using AlarmDispatcher dispatcher = new AlarmDispatcher( consumer, new MxAccessAlarmEventSink(new MxAccessEventQueue(), new MxAccessEventMapper()), SessionId); IReadOnlyList snapshots = dispatcher.SnapshotActiveAlarms(); Assert.Equal(2, snapshots.Count); Assert.Equal("Galaxy!TestArea.Tag1", snapshots[0].AlarmFullReference); Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState); Assert.Equal(500, snapshots[0].Severity); Assert.Equal(ts, snapshots[0].LastTransitionTimestamp.ToDateTime()); Assert.Equal("Galaxy!TestArea.Tag2", snapshots[1].AlarmFullReference); Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState); } [Fact] public void Dispose_WhenSubscribed_UnsubscribesHandlerAndDisposesConsumer() { FakeAlarmConsumer consumer = new FakeAlarmConsumer(); MxAccessEventQueue queue = new MxAccessEventQueue(); MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper()); AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId); dispatcher.Dispose(); Assert.True(consumer.Disposed); consumer.RaiseTransition(new MxAlarmTransitionEvent { PreviousState = MxAlarmStateKind.Unspecified, Record = new MxAlarmSnapshotRecord { AlarmGuid = Guid.NewGuid(), ProviderName = "Galaxy", Group = "G", TagName = "T", State = MxAlarmStateKind.UnackAlm, }, }); Assert.Equal(0, queue.Count); } private sealed class FakeAlarmConsumer : IMxAccessAlarmConsumer { public event EventHandler? AlarmTransitionEmitted; public string? LastSubscription { get; private set; } public Guid LastAckGuid { get; private set; } public string? LastAckComment { get; private set; } public string? LastAckOperatorName { get; private set; } public string? LastAckOperatorNode { get; private set; } public string? LastAckOperatorDomain { get; private set; } public string? LastAckOperatorFullName { get; private set; } public int AcknowledgeReturn { get; set; } public IReadOnlyList SnapshotResult { get; set; } = Array.Empty(); public bool Disposed { get; private set; } public void RaiseTransition(MxAlarmTransitionEvent transition) { AlarmTransitionEmitted?.Invoke(this, transition); } public void Subscribe(string subscription) { LastSubscription = subscription; } public int AcknowledgeByGuid( Guid alarmGuid, string ackComment, string ackOperatorName, string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) { LastAckGuid = alarmGuid; LastAckComment = ackComment; LastAckOperatorName = ackOperatorName; LastAckOperatorNode = ackOperatorNode; LastAckOperatorDomain = ackOperatorDomain; LastAckOperatorFullName = ackOperatorFullName; return AcknowledgeReturn; } public int AcknowledgeByName( string alarmName, string providerName, string groupName, string ackComment, string ackOperatorName, string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName) { LastAckByNameTuple = (alarmName, providerName, groupName); LastAckOperatorName = ackOperatorName; return AcknowledgeReturn; } public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; } public IReadOnlyList SnapshotActiveAlarms() { return SnapshotResult; } public int PollCount { get; private set; } public void PollOnce() { PollCount++; } public void Dispose() { Disposed = true; } } }