test: migrate AdminUI/ControlPlane/Runtime/Host tests to xunit.v3 (#756)
v2-ci / unit-tests (push) Failing after 13m22s

Akka.TestKit.Xunit2 -> Akka.TestKit.Xunit 1.5.62 (xunit.v3 TestKit; needs xunit.v3 3.2.2, set via
VersionOverride on the four projects). Removes the xunit 2.9.3 pin. IAsyncLifetime returns ValueTask,
SqlServerFactAttribute gains the source-info ctor, and xUnit1051 call sites pass
TestContext.Current.CancellationToken. Test counts unchanged.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-10 08:57:33 -05:00
parent 2a8e2981fe
commit b211911f98
191 changed files with 2161 additions and 2166 deletions
@@ -150,7 +150,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
Plan(id: "alm-2", depRef: "M.X", enabled: false), // disabled — not loaded, dep absent
}));
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
reg.TagRefs.ShouldContain("M.T");
reg.TagRefs.ShouldNotContain("M.X");
}
@@ -168,12 +168,12 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
var now = DateTime.UtcNow;
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, now));
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout);
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
// The full Part 9 snapshot bridges through (T15) — every Core condition field maps:
// on activation the engine sets Active, clears Ack AND Confirm (a new active occurrence needs a
@@ -186,7 +186,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
state.State.Severity.ShouldBe((ushort)1000); // 800 → Critical bucket → 1000
state.State.Message.ShouldBe("condition"); // e.Message
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.TransitionKind.ShouldBe("Activated");
evt.Severity.ShouldBe(1000); // 800 → Critical bucket → 1000
@@ -209,20 +209,20 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan() }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Activate first.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Now clear.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 10, DateTime.UtcNow));
var cleared = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => !m.State.Active, Timeout);
var cleared = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => !m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
cleared.AlarmNodeId.ShouldBe("alm-1");
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Cleared", Timeout);
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Cleared", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
}
@@ -236,11 +236,11 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T") }));
var first = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
var first = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
first.TagRefs.ShouldContain("M.T");
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-9", depRef: "M.Y") }));
var second = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
var second = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
second.TagRefs.ShouldContain("M.Y");
second.TagRefs.ShouldNotContain("M.T");
}
@@ -258,11 +258,11 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
// Apply a plan that fails to compile — LoadAsync faults, the host swallows it as a Warning and
// does NOT register interest (no dep refs to register, and the load never completed).
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { BadPlan() }));
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
// Prove the actor is still responsive: a later valid apply loads + registers interest as normal.
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-ok", depRef: "M.T") }));
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
var reg = mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
reg.TagRefs.ShouldContain("M.T");
}
@@ -289,10 +289,10 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-b", depRef: "M.B") }));
// The latest generation must win: fish for the RegisterInterest reflecting the second apply.
mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.B"), Timeout);
mux.FishForMessage<DependencyMuxActor.RegisterInterest>(r => r.TagRefs.Contains("M.B"), Timeout, cancellationToken: TestContext.Current.CancellationToken);
// No LATER RegisterInterest may re-introduce the first (superseded) apply's "M.A" ref.
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
mux.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>Command path: an AlarmCommand("Acknowledge") for an alarm this host owns (and that is
@@ -309,23 +309,23 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Activate so there is something to acknowledge (Acknowledge no-ops on an already-acked alarm).
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active && !m.State.Acknowledged, Timeout);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active && !m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Acknowledge via the command topic — the host owns alm-1, so AcknowledgeAsync runs.
host.Tell(new AlarmCommand(
AlarmId: "alm-1", Operation: "Acknowledge", User: "alice", Comment: "ack-note", UnshelveAtUtc: null));
var acked = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Acknowledged, Timeout);
var acked = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken);
acked.AlarmNodeId.ShouldBe("alm-1");
acked.State.Acknowledged.ShouldBeTrue();
// The transition carries the acting user mapped from cmd.User (proves AcknowledgeAsync got it).
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Acknowledged", Timeout);
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Acknowledged", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.User.ShouldBe("alice");
// Historian feed prep: the operator's ack comment threads through to the transition event.
@@ -345,14 +345,14 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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; host owns only alm-1
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed; host owns only alm-1
// Command targets an alarm this engine never loaded — it must be a no-op.
host.Tell(new AlarmCommand(
AlarmId: "not-mine", Operation: "Acknowledge", User: "alice", Comment: null, UnshelveAtUtc: null));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // no engine op → no projection
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // no engine op → no transition
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // no engine op → no projection
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // no engine op → no transition
}
/// <summary>Validation: an AddComment command with empty or whitespace comment text is rejected (logged),
@@ -370,17 +370,17 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // 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
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // rejected → no engine op → no OPC UA projection
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(200), TestContext.Current.CancellationToken); // 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);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
}
@@ -397,13 +397,13 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // 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);
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "CommentAdded", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
}
@@ -422,14 +422,14 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // 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);
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "CommentAdded", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.User.ShouldBe("carol"); // operator — NOT "system"
evt.Comment.ShouldBe("operator note");
@@ -447,16 +447,16 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// TimedShelve with a null UnshelveAtUtc is malformed — the host rejects + logs, does not throw.
host.Tell(new AlarmCommand(
AlarmId: "alm-1", Operation: "TimedShelve", User: "alice", Comment: null, UnshelveAtUtc: null));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); // rejected → no engine op → no projection
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken); // rejected → no engine op → no projection
// 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);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
}
@@ -474,14 +474,14 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// No RedundancyStateChanged sent — local role unknown ⇒ default-emit.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
// The OPC UA node write happens AND the alerts transition is published.
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.TransitionKind.ShouldBe("Activated");
}
@@ -499,18 +499,18 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Mark this node Secondary, then activate.
TellRedundancyRole(host, RedundancyRole.Secondary);
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
// The local OPC UA node write is UNGATED — it must still arrive.
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
// The cluster-wide alerts publish is gated off on the secondary.
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>Primary publishes (T1): when the cached local role is Primary, the host publishes the
@@ -525,14 +525,14 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Mark this node Primary, then activate.
TellRedundancyRole(host, RedundancyRole.Primary);
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.TransitionKind.ShouldBe("Activated");
}
@@ -550,13 +550,13 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = SpawnWithMemberCount(publish, mux, LocalNode, driverMemberCount: 2);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
// No snapshot sent ⇒ role unknown; count 2 ⇒ default-DENY.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>Boot-window single-driver emit (archreview 03/S4): role unknown + no driver peer (member
@@ -571,12 +571,12 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = SpawnWithMemberCount(publish, mux, LocalNode, driverMemberCount: 1);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
alerts.ExpectMsg<AlarmTransitionEvent>(Timeout).AlarmId.ShouldBe("alm-1");
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
alerts.ExpectMsg<AlarmTransitionEvent>(Timeout, cancellationToken: TestContext.Current.CancellationToken).AlarmId.ShouldBe("alm-1");
}
private (IActorRef Host, DependencyMuxTagUpstreamSource Upstream) SpawnWithMemberCount(
@@ -601,20 +601,20 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T") }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Mark this node Secondary — the alerts publish is gated, but command processing is NOT.
TellRedundancyRole(host, RedundancyRole.Secondary);
// Activate so there is something to acknowledge.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active && !m.State.Acknowledged, Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active && !m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Acknowledge via the command topic — the engine must process it even on the secondary.
host.Tell(new AlarmCommand(
AlarmId: "alm-1", Operation: "Acknowledge", User: "alice", Comment: "ack-note", UnshelveAtUtc: null));
var acked = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Acknowledged, Timeout);
var acked = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Acknowledged, Timeout, cancellationToken: TestContext.Current.CancellationToken);
acked.AlarmNodeId.ShouldBe("alm-1");
acked.State.Acknowledged.ShouldBeTrue();
}
@@ -632,18 +632,18 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Mark this node Detached, then activate.
TellRedundancyRole(host, RedundancyRole.Detached);
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
// The local OPC UA node write is UNGATED — it must still arrive.
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
// The cluster-wide alerts publish is gated off on a detached node.
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>HistorizeToAveva flag threading (middle-link): the host MUST carry the plan's
@@ -667,18 +667,18 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { planTrue, planFalse }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Activate the true-flag alarm.
host.Tell(new VirtualTagActor.DependencyValueChanged("H.T", 99, DateTime.UtcNow));
var evtTrue = alerts.FishForMessage<AlarmTransitionEvent>(
e => e.AlarmId == "alm-hist-true" && e.TransitionKind == "Activated", Timeout);
e => e.AlarmId == "alm-hist-true" && e.TransitionKind == "Activated", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evtTrue.HistorizeToAveva.ShouldBe(true);
// Activate the false-flag alarm.
host.Tell(new VirtualTagActor.DependencyValueChanged("H.F", 99, DateTime.UtcNow));
var evtFalse = alerts.FishForMessage<AlarmTransitionEvent>(
e => e.AlarmId == "alm-hist-false" && e.TransitionKind == "Activated", Timeout);
e => e.AlarmId == "alm-hist-false" && e.TransitionKind == "Activated", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evtFalse.HistorizeToAveva.ShouldBe(false);
}
@@ -695,19 +695,19 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T", threshold: 90) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Baseline is Good (unread inputs are not a quality signal — no load-time annotation). Drive the
// input Bad with a value below threshold: the predicate freezes (no transition), but the worst-input
// quality bucket moves Good→Bad → a QualityChanged annotation flows out of band.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 10, DateTime.UtcNow, OpcUaStatus.Bad));
var q = publish.FishForMessage<OpcUaPublishActor.AlarmQualityUpdate>(m => m.StatusCode == OpcUaStatus.Bad, Timeout);
var q = publish.FishForMessage<OpcUaPublishActor.AlarmQualityUpdate>(m => m.StatusCode == OpcUaStatus.Bad, Timeout, cancellationToken: TestContext.Current.CancellationToken);
q.AlarmNodeId.ShouldBe("alm-1");
q.Realm.ShouldBe(AddressSpaceRealm.Uns);
// A pure quality change is NOT a state transition: no /alerts row.
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(400));
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(400), TestContext.Current.CancellationToken);
}
/// <summary>#478 — when a transition fires while an input is Uncertain, the projected full snapshot
@@ -723,12 +723,12 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(id: "alm-1", depRef: "M.T", threshold: 90) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Above threshold (activates) but Uncertain quality — Uncertain is "ready", so the predicate runs.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow, OpcUaStatus.Uncertain));
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
state.State.StatusCode.ShouldBe(OpcUaStatus.Uncertain);
}
@@ -748,19 +748,19 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
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
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Activate so there is something to shelve.
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout);
publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Activated", Timeout, cancellationToken: TestContext.Current.CancellationToken);
// 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<AlarmTransitionEvent>(e => e.TransitionKind == "Shelved", Timeout);
var evt = alerts.FishForMessage<AlarmTransitionEvent>(e => e.TransitionKind == "Shelved", Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.User.ShouldBe("carol"); // operator — NOT "system"
}
@@ -779,7 +779,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
var (host, _) = Spawn(publish, mux, LocalNode);
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // load completed
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken); // load completed
// Send a snapshot that mentions only a DIFFERENT node — LocalNode is absent.
// The host cannot determine its own role from this snapshot, so the cached role
@@ -789,10 +789,10 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new VirtualTagActor.DependencyValueChanged("M.T", 99, DateTime.UtcNow));
// Both the OPC UA write AND the alerts publish must arrive (default-emit, role unknown).
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout);
var state = publish.FishForMessage<OpcUaPublishActor.AlarmStateUpdate>(m => m.State.Active, Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout);
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
evt.AlarmId.ShouldBe("alm-1");
evt.TransitionKind.ShouldBe("Activated");
}
@@ -852,7 +852,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout);
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.AlarmNodeId.ShouldBe("alm-1");
state.Realm.ShouldBe(AddressSpaceRealm.Uns);
state.State.Active.ShouldBeTrue("the persisted state was Active — the rebuilt node must say so");
@@ -880,10 +880,10 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
// Wait for the re-assert to actually happen FIRST — otherwise the absence window below could
// elapse before the load even completed and would prove nothing.
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout);
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
// Absence assertion: the elapsed time IS the assertion, so this stays deliberately short.
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>An alarm sitting in the Part 9 no-event position already matches what the materialise
@@ -900,8 +900,8 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
// Sequence the absence: the re-assert (if any) happens right after the mux registration, so
// waiting for that first makes the window below cover the moment the write would have landed.
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout);
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
/// <summary>Not just Active: every field a rebuild would silently lose is restored. A shelved (but
@@ -919,7 +919,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan() }));
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout);
var state = publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout, cancellationToken: TestContext.Current.CancellationToken);
state.State.Active.ShouldBeFalse();
state.State.Shelving.ShouldBe(AlarmShelvingKind.OneShot);
}
@@ -936,7 +936,7 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan() }));
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout)
publish.ExpectMsg<OpcUaPublishActor.AlarmStateUpdate>(Timeout, cancellationToken: TestContext.Current.CancellationToken)
.TimestampUtc.ShouldBe(wentActiveAt);
}
@@ -952,6 +952,6 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(enabled: false) }));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500), TestContext.Current.CancellationToken);
}
}