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 0961f4e5..adb0e8dd 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 @@ -98,38 +98,32 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase /// 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.) + /// + /// + /// To reproduce the race deterministically: a ForceReconnect is enqueued first, then + /// the driver fires an alarm while its OnAlarmEvent handler is still attached (the actor + /// hasn't yet processed ForceReconnect). The handler's self.Tell(NativeAlarmRaised) + /// lands second in the mailbox. The actor then processes ForceReconnect → Reconnecting + /// (detaches handler), then processes the queued NativeAlarmRaised in Reconnecting + /// → drops it. The default 10 s reconnect interval ensures no retry fires during the check. + /// /// [Fact] public void Native_alarm_during_reconnect_is_dropped_not_forwarded() { + // Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window. var driver = new AlarmSourceStubDriver(); var parent = CreateTestProbe(); - var actor = parent.ChildActorOf(DriverInstanceActor.Props( - driver, reconnectInterval: TimeSpan.FromMilliseconds(50))); + var actor = parent.ChildActorOf(DriverInstanceActor.Props(driver)); - // Drive the actor to Connected first, then push it into Reconnecting via DisconnectObserved. + // Drive to Connected; confirm the alarm handler is attached. 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. + // Enqueue ForceReconnect FIRST — the actor hasn't processed it yet, so the handler is + // still wired. The test thread then immediately fires the alarm on the driver; the handler's + // self.Tell(NativeAlarmRaised) lands SECOND in the mailbox. + actor.Tell(new DriverInstanceActor.ForceReconnect()); driver.RaiseAlarm(new AlarmEventArgs( new StubAlarmHandle(), SourceNodeId: "src-node-reconnect", @@ -140,12 +134,15 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase 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 processes: (1) ForceReconnect → Reconnecting (handler detached); + // (2) NativeAlarmRaised → dropped (debug log, no forward). + // The parent must NOT receive AttributeAlarmPublished from that alarm. + // Wait generously — the default reconnect interval of 10 s means no retry fires here. + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); - // The actor must still be alive (not crashed/dead-lettered) — a Watch + no Terminated proves it. + // The actor must still be alive — Watch + no Terminated (not crashed or dead-lettered). Watch(actor); - parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); // still no Terminated + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); } ///