feat(worker): attribute alarm ack transitions from wnwrap ACK fields
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
@@ -17,6 +17,38 @@ public sealed class AlarmDispatcherTests
|
|||||||
{
|
{
|
||||||
private const string SessionId = "session-001";
|
private const string SessionId = "session-001";
|
||||||
|
|
||||||
|
/// <summary>Verifies an Acknowledge transition is attributed to the ACK_* operator and comment.</summary>
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that alarm transitions land in the queue with correctly mapped fields.</summary>
|
/// <summary>Verifies that alarm transitions land in the queue with correctly mapped fields.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnTransition_WhenAlarmTransitionRaised_LandsInQueueWithMappedFields()
|
public void OnTransition_WhenAlarmTransitionRaised_LandsInQueueWithMappedFields()
|
||||||
|
|||||||
@@ -801,4 +801,27 @@ public sealed class WnWrapAlarmConsumerXmlTests
|
|||||||
ProviderName = "Galaxy",
|
ProviderName = "Galaxy",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseSnapshotXml_WithAckElements_PopulatesAckFields()
|
||||||
|
{
|
||||||
|
const string xml =
|
||||||
|
"<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"1\"><ALARM><GUID>2D8A9BE88FE04B0F8D996BB4D56620C7</GUID>" +
|
||||||
|
"<DATE>2026/10/19</DATE><TIME>18:2:53.812</TIME><GMTOFFSET>240</GMTOFFSET><DSTADJUST>0</DSTADJUST>" +
|
||||||
|
"<PROVIDER_NODE>DESKTOP-6JL3KKO</PROVIDER_NODE><PROVIDER_NAME>Galaxy</PROVIDER_NAME><GROUP>TestArea</GROUP>" +
|
||||||
|
"<TAGNAME>TestMachine_001.TestAlarm001</TAGNAME><TYPE>DSC</TYPE><VALUE>true</VALUE><LIMIT>true</LIMIT>" +
|
||||||
|
"<PRIORITY>500</PRIORITY><STATE>ACK_ALM</STATE><OPERATOR_NODE></OPERATOR_NODE><OPERATOR_NAME></OPERATOR_NAME>" +
|
||||||
|
"<ACK_DOMAIN_NAME>ArchestrA</ACK_DOMAIN_NAME><ACK_OPERATOR_NODE>mxa</ACK_OPERATOR_NODE>" +
|
||||||
|
"<ACK_OPERATOR_NAME>Administrator</ACK_OPERATOR_NAME><ALARM_COMMENT>Test alarm #1</ALARM_COMMENT>" +
|
||||||
|
"<ACK_COMMENT>mgack-authwrite 180714</ACK_COMMENT></ALARM></ALARM_RECORDS>";
|
||||||
|
|
||||||
|
Dictionary<Guid, MxAlarmSnapshotRecord> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ public sealed class AlarmDispatcher : IDisposable
|
|||||||
|
|
||||||
string fullReference = AlarmRecordTransitionMapper.ComposeFullReference(
|
string fullReference = AlarmRecordTransitionMapper.ComposeFullReference(
|
||||||
record.ProviderName, record.Group, record.TagName);
|
record.ProviderName, record.Group, record.TagName);
|
||||||
|
bool useAck = kind == AlarmTransitionKind.Acknowledge && HasAckAttribution(record);
|
||||||
|
|
||||||
sink.EnqueueTransition(
|
sink.EnqueueTransition(
|
||||||
alarmFullReference: fullReference,
|
alarmFullReference: fullReference,
|
||||||
@@ -253,15 +254,22 @@ public sealed class AlarmDispatcher : IDisposable
|
|||||||
severity: record.Priority,
|
severity: record.Priority,
|
||||||
originalRaiseTimestampUtc: null,
|
originalRaiseTimestampUtc: null,
|
||||||
transitionTimestampUtc: record.TransitionTimestampUtc,
|
transitionTimestampUtc: record.TransitionTimestampUtc,
|
||||||
operatorUser: record.OperatorName,
|
operatorUser: useAck ? record.AckOperatorName : record.OperatorName,
|
||||||
operatorComment: record.AlarmComment,
|
operatorComment: useAck ? record.AckComment : record.AlarmComment,
|
||||||
category: record.Group,
|
category: record.Group,
|
||||||
description: string.Empty,
|
description: string.Empty,
|
||||||
degraded: record.Degraded);
|
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)
|
private static ActiveAlarmSnapshot MapToSnapshot(MxAlarmSnapshotRecord record, bool truncated)
|
||||||
{
|
{
|
||||||
|
bool useAck = IsAcked(record.State) && HasAckAttribution(record);
|
||||||
ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot
|
ActiveAlarmSnapshot snapshot = new ActiveAlarmSnapshot
|
||||||
{
|
{
|
||||||
AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference(
|
AlarmFullReference = AlarmRecordTransitionMapper.ComposeFullReference(
|
||||||
@@ -270,8 +278,8 @@ public sealed class AlarmDispatcher : IDisposable
|
|||||||
AlarmTypeName = record.Type,
|
AlarmTypeName = record.Type,
|
||||||
CurrentState = MapConditionState(record.State),
|
CurrentState = MapConditionState(record.State),
|
||||||
Severity = record.Priority,
|
Severity = record.Priority,
|
||||||
OperatorUser = record.OperatorName,
|
OperatorUser = useAck ? record.AckOperatorName : record.OperatorName,
|
||||||
OperatorComment = record.AlarmComment,
|
OperatorComment = useAck ? record.AckComment : record.AlarmComment,
|
||||||
Category = record.Group,
|
Category = record.Group,
|
||||||
Description = string.Empty,
|
Description = string.Empty,
|
||||||
Degraded = record.Degraded,
|
Degraded = record.Degraded,
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ public sealed class MxAlarmSnapshotRecord
|
|||||||
public string OperatorName { get; set; } = string.Empty;
|
public string OperatorName { get; set; } = string.Empty;
|
||||||
/// <summary>Gets or sets the alarm comment.</summary>
|
/// <summary>Gets or sets the alarm comment.</summary>
|
||||||
public string AlarmComment { get; set; } = string.Empty;
|
public string AlarmComment { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the acknowledgement comment (wnwrap <c>ACK_COMMENT</c>).</summary>
|
||||||
|
public string AckComment { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the acknowledging operator's name (wnwrap <c>ACK_OPERATOR_NAME</c>).</summary>
|
||||||
|
public string AckOperatorName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the acknowledging operator's domain (wnwrap <c>ACK_DOMAIN_NAME</c>).</summary>
|
||||||
|
public string AckOperatorDomain { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the acknowledging node, i.e. the MXAccess client name (wnwrap <c>ACK_OPERATOR_NODE</c>).</summary>
|
||||||
|
public string AckOperatorNode { get; set; } = string.Empty;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether this record was synthesized
|
/// Gets or sets a value indicating whether this record was synthesized
|
||||||
/// by the subtag-provider fallback rather than emitted by the native
|
/// by the subtag-provider fallback rather than emitted by the native
|
||||||
|
|||||||
@@ -792,6 +792,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
|||||||
string? operatorNode = null;
|
string? operatorNode = null;
|
||||||
string? operatorName = null;
|
string? operatorName = null;
|
||||||
string? alarmComment = null;
|
string? alarmComment = null;
|
||||||
|
string? ackComment = null;
|
||||||
|
string? ackOperatorName = null;
|
||||||
|
string? ackOperatorDomain = null;
|
||||||
|
string? ackOperatorNode = null;
|
||||||
|
|
||||||
foreach (XmlNode child in alarmNode.ChildNodes)
|
foreach (XmlNode child in alarmNode.ChildNodes)
|
||||||
{
|
{
|
||||||
@@ -816,6 +820,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
|||||||
case "OPERATOR_NODE": operatorNode ??= child.InnerText; break;
|
case "OPERATOR_NODE": operatorNode ??= child.InnerText; break;
|
||||||
case "OPERATOR_NAME": operatorName ??= child.InnerText; break;
|
case "OPERATOR_NAME": operatorName ??= child.InnerText; break;
|
||||||
case "ALARM_COMMENT": alarmComment ??= 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,
|
OperatorNode = operatorNode ?? string.Empty,
|
||||||
OperatorName = operatorName ?? string.Empty,
|
OperatorName = operatorName ?? string.Empty,
|
||||||
AlarmComment = alarmComment ?? string.Empty,
|
AlarmComment = alarmComment ?? string.Empty,
|
||||||
|
AckComment = ackComment ?? string.Empty,
|
||||||
|
AckOperatorName = ackOperatorName ?? string.Empty,
|
||||||
|
AckOperatorDomain = ackOperatorDomain ?? string.Empty,
|
||||||
|
AckOperatorNode = ackOperatorNode ?? string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return records;
|
return records;
|
||||||
|
|||||||
Reference in New Issue
Block a user