diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs index 7c85796..677dc37 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs @@ -17,6 +17,38 @@ public sealed class AlarmDispatcherTests { private const string SessionId = "session-001"; + /// Verifies an Acknowledge transition is attributed to the ACK_* operator and comment. + [Fact] + public void OnTransition_WhenAcknowledged_UsesAckOperatorAndComment() + { + 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 = "TestArea", + TagName = "TestMachine_001.TestAlarm001", + State = MxAlarmStateKind.AckAlm, + AlarmComment = "Test alarm #1", + AckOperatorName = "alice", + AckComment = "checked", + }, + }); + + Assert.True(queue.TryDequeue(out WorkerEvent? evt)); + OnAlarmTransitionEvent body = evt!.Event.OnAlarmTransition; + Assert.Equal(AlarmTransitionKind.Acknowledge, body.TransitionKind); + Assert.Equal("alice", body.OperatorUser); + Assert.Equal("checked", body.OperatorComment); + } + /// Verifies that alarm transitions land in the queue with correctly mapped fields. [Fact] public void OnTransition_WhenAlarmTransitionRaised_LandsInQueueWithMappedFields() diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs index afef255..3941a21 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs @@ -801,4 +801,27 @@ public sealed class WnWrapAlarmConsumerXmlTests ProviderName = "Galaxy", }; } + + [Fact] + public void ParseSnapshotXml_WithAckElements_PopulatesAckFields() + { + const string xml = + "2D8A9BE88FE04B0F8D996BB4D56620C7" + + "2026/10/192400" + + "DESKTOP-6JL3KKOGalaxyTestArea" + + "TestMachine_001.TestAlarm001DSCtruetrue" + + "500ACK_ALM" + + "ArchestrAmxa" + + "AdministratorTest alarm #1" + + "mgack-authwrite 180714"; + + Dictionary records = WnWrapAlarmConsumer.ParseSnapshotXml(xml); + + MxAlarmSnapshotRecord record = Assert.Single(records.Values); + Assert.Equal("Administrator", record.AckOperatorName); + Assert.Equal("ArchestrA", record.AckOperatorDomain); + Assert.Equal("mxa", record.AckOperatorNode); + Assert.Equal("mgack-authwrite 180714", record.AckComment); + Assert.Equal("Test alarm #1", record.AlarmComment); + } } diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs index 388266e..722db2e 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs @@ -244,6 +244,7 @@ public sealed class AlarmDispatcher : IDisposable string fullReference = AlarmRecordTransitionMapper.ComposeFullReference( record.ProviderName, record.Group, record.TagName); + bool useAck = kind == AlarmTransitionKind.Acknowledge && HasAckAttribution(record); sink.EnqueueTransition( alarmFullReference: fullReference, @@ -253,15 +254,22 @@ public sealed class AlarmDispatcher : IDisposable severity: record.Priority, originalRaiseTimestampUtc: null, transitionTimestampUtc: record.TransitionTimestampUtc, - operatorUser: record.OperatorName, - operatorComment: record.AlarmComment, + operatorUser: useAck ? record.AckOperatorName : record.OperatorName, + operatorComment: useAck ? record.AckComment : record.AlarmComment, category: record.Group, description: string.Empty, degraded: record.Degraded); } + private static bool HasAckAttribution(MxAlarmSnapshotRecord record) => + !string.IsNullOrEmpty(record.AckOperatorName) || !string.IsNullOrEmpty(record.AckComment); + + private static bool IsAcked(MxAlarmStateKind state) => + state == MxAlarmStateKind.AckAlm || state == MxAlarmStateKind.AckRtn; + private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record, bool truncated) { + bool useAck = IsAcked(record.State) && HasAckAttribution(record); ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot { AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference( @@ -270,8 +278,8 @@ public sealed class AlarmDispatcher : IDisposable AlarmTypeName = record.Type, CurrentState = MapConditionState(record.State), Severity = record.Priority, - OperatorUser = record.OperatorName, - OperatorComment = record.AlarmComment, + OperatorUser = useAck ? record.AckOperatorName : record.OperatorName, + OperatorComment = useAck ? record.AckComment : record.AlarmComment, Category = record.Group, Description = string.Empty, Degraded = record.Degraded, diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs index b8556d2..c431728 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAlarmSnapshot.cs @@ -39,6 +39,18 @@ public sealed class MxAlarmSnapshotRecord public string OperatorName { get; set; } = string.Empty; /// Gets or sets the alarm comment. public string AlarmComment { get; set; } = string.Empty; + + /// Gets or sets the acknowledgement comment (wnwrap ACK_COMMENT). + public string AckComment { get; set; } = string.Empty; + + /// Gets or sets the acknowledging operator's name (wnwrap ACK_OPERATOR_NAME). + public string AckOperatorName { get; set; } = string.Empty; + + /// Gets or sets the acknowledging operator's domain (wnwrap ACK_DOMAIN_NAME). + public string AckOperatorDomain { get; set; } = string.Empty; + + /// Gets or sets the acknowledging node, i.e. the MXAccess client name (wnwrap ACK_OPERATOR_NODE). + public string AckOperatorNode { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this record was synthesized /// by the subtag-provider fallback rather than emitted by the native diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs index 5f3a7f4..f109dc3 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs @@ -792,6 +792,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer string? operatorNode = null; string? operatorName = null; string? alarmComment = null; + string? ackComment = null; + string? ackOperatorName = null; + string? ackOperatorDomain = null; + string? ackOperatorNode = null; foreach (XmlNode child in alarmNode.ChildNodes) { @@ -816,6 +820,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer case "OPERATOR_NODE": operatorNode ??= child.InnerText; break; case "OPERATOR_NAME": operatorName ??= child.InnerText; break; case "ALARM_COMMENT": alarmComment ??= child.InnerText; break; + case "ACK_COMMENT": ackComment ??= child.InnerText; break; + case "ACK_OPERATOR_NAME": ackOperatorName ??= child.InnerText; break; + case "ACK_DOMAIN_NAME": ackOperatorDomain ??= child.InnerText; break; + case "ACK_OPERATOR_NODE": ackOperatorNode ??= child.InnerText; break; } } @@ -843,6 +851,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer OperatorNode = operatorNode ?? string.Empty, OperatorName = operatorName ?? string.Empty, AlarmComment = alarmComment ?? string.Empty, + AckComment = ackComment ?? string.Empty, + AckOperatorName = ackOperatorName ?? string.Empty, + AckOperatorDomain = ackOperatorDomain ?? string.Empty, + AckOperatorNode = ackOperatorNode ?? string.Empty, }; } return records;