fix(runtime): reject empty AddComment instead of silently swallowing it
Validate AddComment up-front (IsNullOrWhiteSpace guard + Warning log) so a blank-comment command is cleanly rejected before reaching the engine rather than faulting inside ApplyAddComment and being silently swallowed by the outer catch. Mirrors the existing TimedShelve missing-UnshelveAtUtc pattern. Also fix two stale inline comments: the "async void crash" note on TimedShelve now correctly says "fault escaping async Task → supervision restart", and the ownership-filter now documents the benign race with a concurrent LoadAsync clearing the loaded set. Tests: AlarmCommand_add_comment_empty_text_is_rejected_not_driven (Theory — empty string + whitespace) and AlarmCommand_add_comment_nonempty_drives_engine (positive path, asserts CommentAdded transition on alerts topic).
This commit is contained in:
+52
@@ -305,6 +305,58 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // no engine op → no transition
|
||||
}
|
||||
|
||||
/// <summary>Validation: an AddComment command with empty or whitespace comment text is rejected (logged),
|
||||
/// NOT propagated to the engine — the actor stays alive and still processes a subsequent valid command,
|
||||
/// proving it didn't fault and the engine's AddCommentAsync was never driven.</summary>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void AlarmCommand_add_comment_empty_text_is_rejected_not_driven(string emptyComment)
|
||||
{
|
||||
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
|
||||
|
||||
// AddComment with empty/whitespace text is rejected before reaching the engine.
|
||||
host.Tell(new AlarmCommand(
|
||||
AlarmId: "alm-1", Operation: "AddComment", User: "alice", Comment: emptyComment, UnshelveAtUtc: null));
|
||||
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // rejected → no engine op → no OPC UA projection
|
||||
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); // rejected → no alerts event
|
||||
|
||||
// Prove the actor survived: activate the alarm and observe the normal projection flow.
|
||||
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
|
||||
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
|
||||
state.AlarmNodeId.ShouldBe("alm-1");
|
||||
}
|
||||
|
||||
/// <summary>Positive AddComment path: a non-empty AddComment for a loaded alarm drives the engine's
|
||||
/// AddCommentAsync — observed via an AlarmTransitionEvent("CommentAdded") on the alerts topic carrying
|
||||
/// the acting user (proves the op ran end-to-end through the host dispatch).</summary>
|
||||
[Fact]
|
||||
public void AlarmCommand_add_comment_nonempty_drives_engine()
|
||||
{
|
||||
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
|
||||
|
||||
// AddComment with a non-empty comment drives the engine — CommentAdded transition emitted.
|
||||
host.Tell(new AlarmCommand(
|
||||
AlarmId: "alm-1", Operation: "AddComment", User: "bob", Comment: "note from operator", UnshelveAtUtc: null));
|
||||
|
||||
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "CommentAdded", Timeout);
|
||||
evt.AlarmId.ShouldBe("alm-1");
|
||||
}
|
||||
|
||||
/// <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]
|
||||
|
||||
Reference in New Issue
Block a user