fix(alarms): historize the operator (not 'system') for CommentAdded transitions (review)
v2-ci / build (push) Failing after 50s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

This commit is contained in:
Joseph Doherty
2026-06-11 11:42:56 -04:00
parent 6dbd4fb875
commit f64f7ce669
2 changed files with 32 additions and 1 deletions
@@ -502,11 +502,14 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor
/// <summary>The acting user for an <see cref="AlarmTransitionEvent"/>. Engine-driven
/// Activated / Cleared transitions are <c>"system"</c>; operator Acknowledged / Confirmed carry the
/// recorded user from the condition state, falling back to <c>"system"</c> when none was recorded.</summary>
/// recorded user from the condition state, falling back to <c>"system"</c> when none was recorded.
/// CommentAdded carries the commenter's identity from the last entry in
/// <see cref="AlarmConditionState.Comments"/>.</summary>
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",
};
@@ -389,6 +389,34 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
evt.AlarmId.ShouldBe("alm-1");
}
/// <summary>CommentAdded transition carries the commenter's identity: a non-empty AddComment drives
/// AddCommentAsync — the resulting AlarmTransitionEvent("CommentAdded") on the alerts topic must carry
/// <c>User == cmd.User</c> (the acting operator), NOT the generic <c>"system"</c> sentinel that
/// engine-driven transitions use. This verifies the <see cref="ScriptedAlarmHostActor"/>
/// TransitionUser arm for <see cref="EmissionKind.CommentAdded"/>.</summary>
[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<DependencyMuxActor.RegisterInterest>(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<AlarmTransitionEvent>(e => e.TransitionKind == "CommentAdded", Timeout);
evt.AlarmId.ShouldBe("alm-1");
evt.User.ShouldBe("carol"); // operator — NOT "system"
evt.Comment.ShouldBe("operator note");
}
/// <summary>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.</summary>
[Fact]