From f64f7ce66902f7c1e50f79105519a161185fde7c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 11 Jun 2026 11:42:56 -0400 Subject: [PATCH] fix(alarms): historize the operator (not 'system') for CommentAdded transitions (review) --- .../ScriptedAlarms/ScriptedAlarmHostActor.cs | 5 +++- .../ScriptedAlarmHostActorTests.cs | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs index 44091b70..6509e337 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs @@ -502,11 +502,14 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor /// The acting user for an . Engine-driven /// Activated / Cleared transitions are "system"; operator Acknowledged / Confirmed carry the - /// recorded user from the condition state, falling back to "system" when none was recorded. + /// recorded user from the condition state, falling back to "system" when none was recorded. + /// CommentAdded carries the commenter's identity from the last entry in + /// . private static string TransitionUser(ScriptedAlarmEvent e) => e.Emission switch { EmissionKind.Acknowledged => e.Condition.LastAckUser ?? "system", EmissionKind.Confirmed => e.Condition.LastConfirmUser ?? "system", + EmissionKind.CommentAdded => e.Condition.Comments.Count > 0 ? e.Condition.Comments[^1].User : "system", _ => "system", }; diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs index 34bee4ca..5dafa92b 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmHostActorTests.cs @@ -389,6 +389,34 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase evt.AlarmId.ShouldBe("alm-1"); } + /// CommentAdded transition carries the commenter's identity: a non-empty AddComment drives + /// AddCommentAsync — the resulting AlarmTransitionEvent("CommentAdded") on the alerts topic must carry + /// User == cmd.User (the acting operator), NOT the generic "system" sentinel that + /// engine-driven transitions use. This verifies the + /// TransitionUser arm for . + [Fact] + public void CommentAdded_transition_carries_operator_user() + { + var publish = CreateTestProbe(); + var mux = CreateTestProbe(); + var alerts = CreateTestProbe(); + SubscribeToAlerts(alerts); + + var (host, _) = Spawn(publish, mux); + host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T") })); + mux.ExpectMsg(Timeout); // load completed + + // Send AddComment — the alarm does NOT need to be active; AddCommentAsync is unconditional. + host.Tell(new AlarmCommand( + AlarmId: "alm-1", Operation: "AddComment", User: "carol", Comment: "operator note", UnshelveAtUtc: null)); + + // The CommentAdded AlarmTransitionEvent must carry the operator's identity, not "system". + var evt = alerts.FishForMessage(e => e.TransitionKind == "CommentAdded", Timeout); + evt.AlarmId.ShouldBe("alm-1"); + evt.User.ShouldBe("carol"); // operator — NOT "system" + evt.Comment.ShouldBe("operator note"); + } + /// Validation: a TimedShelve command missing UnshelveAtUtc is rejected (logged), NOT thrown — /// the actor stays alive and still processes a subsequent valid command, proving it didn't fault. [Fact]