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]