test(redundancy): cover Detached suppression + absent-node default-emit (A1)
This commit is contained in:
+67
-3
@@ -89,19 +89,24 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
}
|
||||
|
||||
/// <summary>Tell the host a <see cref="RedundancyStateChanged"/> snapshot marking
|
||||
/// <see cref="LocalNode"/> with <paramref name="role"/> so the gate observes the local role.</summary>
|
||||
private static void TellRedundancyRole(IActorRef host, RedundancyRole role) =>
|
||||
/// <paramref name="nodeId"/> (defaults to <see cref="LocalNode"/>) with <paramref name="role"/>
|
||||
/// so the gate observes the local role. Pass a different <paramref name="nodeId"/> to produce a
|
||||
/// snapshot that does NOT contain the host's own node.</summary>
|
||||
private static void TellRedundancyRole(IActorRef host, RedundancyRole role, NodeId? nodeId = null)
|
||||
{
|
||||
var id = nodeId ?? LocalNode;
|
||||
host.Tell(new RedundancyStateChanged(
|
||||
new[]
|
||||
{
|
||||
new NodeRedundancyState(
|
||||
NodeId: LocalNode,
|
||||
NodeId: id,
|
||||
Role: role,
|
||||
IsClusterLeader: role == RedundancyRole.Primary,
|
||||
IsRoleLeaderForDriver: role == RedundancyRole.Primary,
|
||||
AsOfUtc: DateTime.UtcNow),
|
||||
},
|
||||
CorrelationId.NewId()));
|
||||
}
|
||||
|
||||
/// <summary>Subscribe <paramref name="probe"/> to the <c>alerts</c> DPS topic and wait for the ack.
|
||||
/// The Subscribe is sent FROM the probe so the SubscribeAck returns to it.</summary>
|
||||
@@ -508,4 +513,63 @@ public sealed class ScriptedAlarmHostActorTests : RuntimeActorTestBase
|
||||
acked.AlarmNodeId.ShouldBe("alm-1");
|
||||
acked.State.Acknowledged.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>Detached suppression (A1): when the cached local role is Detached the host MUST NOT
|
||||
/// publish the cluster-wide alerts transition (identical gating as Secondary) — but it MUST still
|
||||
/// write the local OPC UA condition node so the detached node's address space stays warm.</summary>
|
||||
[Fact]
|
||||
public void Detached_node_suppresses_alerts_publish_but_still_writes_opcua()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var alerts = CreateTestProbe();
|
||||
SubscribeToAlerts(alerts);
|
||||
|
||||
var (host, _) = Spawn(publish, mux, LocalNode);
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // 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);
|
||||
state.AlarmNodeId.ShouldBe("alm-1");
|
||||
|
||||
// The cluster-wide alerts publish is gated off on a detached node.
|
||||
alerts.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
/// <summary>Absent-node default-emit (A1): a <see cref="RedundancyStateChanged"/> snapshot that
|
||||
/// contains ONLY other nodes (the host's own <see cref="LocalNode"/> is absent) must leave the
|
||||
/// cached local role unchanged (null/unknown) — the host therefore defaults to emit, publishing
|
||||
/// the alerts transition AND writing the OPC UA node, exactly as in the boot-window case.</summary>
|
||||
[Fact]
|
||||
public void Redundancy_snapshot_without_local_node_leaves_role_unknown_and_emits()
|
||||
{
|
||||
var publish = CreateTestProbe();
|
||||
var mux = CreateTestProbe();
|
||||
var alerts = CreateTestProbe();
|
||||
SubscribeToAlerts(alerts);
|
||||
|
||||
var (host, _) = Spawn(publish, mux, LocalNode);
|
||||
host.Tell(new ScriptedAlarmHostActor.ApplyScriptedAlarms(new[] { Plan(severity: 800) }));
|
||||
mux.ExpectMsg<DependencyMuxActor.RegisterInterest>(Timeout); // 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
|
||||
// stays null (unknown) ⇒ treated as Primary ⇒ default-emit path.
|
||||
TellRedundancyRole(host, RedundancyRole.Primary, nodeId: new NodeId("some-other-node"));
|
||||
|
||||
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);
|
||||
state.AlarmNodeId.ShouldBe("alm-1");
|
||||
|
||||
var evt = alerts.ExpectMsg<AlarmTransitionEvent>(Timeout);
|
||||
evt.AlarmId.ShouldBe("alm-1");
|
||||
evt.TransitionKind.ShouldBe("Activated");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user