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