test(alarms): guard native-alarm-during-reconnect is dropped not dead-lettered

This commit is contained in:
Joseph Doherty
2026-06-14 22:41:08 -04:00
parent a1d333869e
commit c8db5767ea
@@ -91,6 +91,63 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
}
/// <summary>
/// A native alarm transition that races in while the actor is in <c>Reconnecting</c> is silently
/// dropped (debug log only) — it NEVER reaches the parent as
/// <see cref="DriverInstanceActor.AttributeAlarmPublished"/> and does NOT dead-letter. The feed
/// re-delivers active alarms once the actor re-enters <c>Connected</c>, so dropping here is safe.
/// (<see cref="DriverInstanceActor"/> line ~345: the <c>Reconnecting</c> state's
/// <c>Receive&lt;NativeAlarmRaised&gt;</c> logs a debug message and discards.)
/// </summary>
[Fact]
public void Native_alarm_during_reconnect_is_dropped_not_forwarded()
{
var driver = new AlarmSourceStubDriver();
var parent = CreateTestProbe();
var actor = parent.ChildActorOf(DriverInstanceActor.Props(
driver, reconnectInterval: TimeSpan.FromMilliseconds(50)));
// Drive the actor to Connected first, then push it into Reconnecting via DisconnectObserved.
actor.Tell(new DriverInstanceActor.InitializeRequested("{}"));
AwaitCondition(() => driver.AlarmSubscriberCount == 1, TimeSpan.FromSeconds(2));
// One successful alarm-forward from Connected, to confirm the happy path is hot before the test.
driver.RaiseAlarm(new AlarmEventArgs(
new StubAlarmHandle(),
SourceNodeId: "src-node-reconnect",
ConditionId: "cond-reconnect",
AlarmType: "T",
Message: "pre-reconnect raise",
Severity: AlarmSeverity.High,
SourceTimestampUtc: DateTime.UtcNow,
Kind: AlarmTransitionKind.Raise));
parent.ExpectMsg<DriverInstanceActor.AttributeAlarmPublished>(TimeSpan.FromSeconds(2));
// Force Connected → Reconnecting; the alarm handler is detached during this transition.
actor.Tell(new DriverInstanceActor.DisconnectObserved("test-induced disconnect"));
// Wait until the actor detaches the alarm handler (AlarmSubscriberCount drops to 0).
AwaitCondition(() => driver.AlarmSubscriberCount == 0, TimeSpan.FromSeconds(2));
// Now tell the actor a NativeAlarmRaised (the driver thread can fire this at any time).
// In Reconnecting the actor handles NativeAlarmRaised with a debug log and drops it.
driver.RaiseAlarm(new AlarmEventArgs(
new StubAlarmHandle(),
SourceNodeId: "src-node-reconnect",
ConditionId: "cond-reconnect",
AlarmType: "T",
Message: "during-reconnect raise",
Severity: AlarmSeverity.High,
SourceTimestampUtc: DateTime.UtcNow,
Kind: AlarmTransitionKind.Raise));
// The parent must NOT receive an AttributeAlarmPublished while the actor is Reconnecting.
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300));
// The actor must still be alive (not crashed/dead-lettered) — a Watch + no Terminated proves it.
Watch(actor);
parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); // still no Terminated
}
/// <summary>
/// After a full reconnect cycle (Connected → Reconnecting → Connected), a single raised alarm still
/// yields EXACTLY ONE <see cref="DriverInstanceActor.AttributeAlarmPublished"/> — the