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 76c2903c..044a7e9f 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs
@@ -512,7 +512,13 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor
{
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",
+ // Shelve / unshelve / enable / disable / comment ops each append the acting user as the LAST audit
+ // entry on the emitted condition (engine auto-unshelve appends "system"); read it from there.
+ EmissionKind.CommentAdded
+ or EmissionKind.Shelved
+ or EmissionKind.Unshelved
+ or EmissionKind.Enabled
+ or EmissionKind.Disabled => 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 c096e9ef..94e7a78c 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
@@ -612,6 +612,38 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
evtFalse.HistorizeToAveva.ShouldBe(false);
}
+ /// OneShotShelve transition carries the operator's identity: an operator-driven OneShotShelve
+ /// drives OneShotShelveAsync — the resulting ("Shelved")
+ /// on the alerts topic must carry User == cmd.User (the acting operator), NOT the generic
+ /// "system" sentinel. This verifies the
+ /// TransitionUser arm for .
+ [Fact]
+ public void Shelved_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
+
+ // Activate so there is something to shelve.
+ host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
+ publish.FishForMessage(m => m.State.Active, Timeout);
+ alerts.FishForMessage(e => e.TransitionKind == "Activated", Timeout);
+
+ // Shelve via a one-shot shelve command — the host owns alm-1, so OneShotShelveAsync runs.
+ host.Tell(new AlarmCommand(
+ AlarmId: "alm-1", Operation: "OneShotShelve", User: "carol", Comment: null, UnshelveAtUtc: null));
+
+ // The Shelved AlarmTransitionEvent must carry the operator's identity, not "system".
+ var evt = alerts.FishForMessage(e => e.TransitionKind == "Shelved", Timeout);
+ evt.AlarmId.ShouldBe("alm-1");
+ evt.User.ShouldBe("carol"); // operator — NOT "system"
+ }
+
/// Absent-node default-emit (A1): a snapshot that
/// contains ONLY other nodes (the host's own is absent) must leave the
/// cached local role unchanged (null/unknown) — the host therefore defaults to emit, publishing