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));
}
///