diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs index 2a239e18..0961f4e5 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs @@ -91,6 +91,63 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); } + /// + /// A native alarm transition that races in while the actor is in Reconnecting is silently + /// dropped (debug log only) — it NEVER reaches the parent as + /// and does NOT dead-letter. The feed + /// re-delivers active alarms once the actor re-enters Connected, so dropping here is safe. + /// ( line ~345: the Reconnecting state's + /// Receive<NativeAlarmRaised> logs a debug message and discards.) + /// + [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(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 + } + /// /// After a full reconnect cycle (Connected → Reconnecting → Connected), a single raised alarm still /// yields EXACTLY ONE — the