diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index c062e7ee..b2ca2708 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -421,6 +421,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers ResubscribeDesired(); AttachAlarmSource(); SubscribeDesiredAlarms(); + StartDiscovery(); // re-run discovery on reconnect — keeps the injected tree fresh if the backend's capabilities changed }); // A failure here is a no-op regardless of generation — the retry timer keeps trying the // current config; only a (generation-matched) InitializeSucceeded transitions state. diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs index a97542c7..f880fc3e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorDiscoveryTests.cs @@ -77,6 +77,47 @@ public sealed class DriverInstanceActorDiscoveryTests : RuntimeActorTestBase parent.ExpectNoMsg(TimeSpan.FromMilliseconds(300)); } + /// + /// Discovery RE-RUNS on every return to Connected: after the initial discovery settles, a + /// drives the actor through Reconnecting and + /// back to Connected (via the auto-retry timer, the same path the existing reconnect tests use), + /// and a fresh bounded discovery loop fires — keeping the injected tree current if the backend's + /// capabilities changed across the reconnect. The new init bumps the generation, so any + /// pre-reconnect tick is discarded by the generation guard (the initial loop has already settled + /// here, so none are in flight). + /// + [Fact] + public void Discovery_reruns_after_reconnect() + { + var driver = new DiscoverableStubDriver(); + var parent = CreateTestProbe(); + // Tiny reconnect + rediscover intervals so the whole reconnect-then-rediscover cycle runs fast. + var actor = parent.ChildActorOf(DriverInstanceActor.Props( + driver, + reconnectInterval: TimeSpan.FromMilliseconds(50), + rediscoverInterval: TimeSpan.FromMilliseconds(20))); + + actor.Tell(new DriverInstanceActor.InitializeRequested("{}")); + + // Drain the initial settling passes (0,0,3,3) and confirm the first loop stopped. + for (var i = 0; i < 4; i++) + parent.ExpectMsg(TimeSpan.FromSeconds(2)); + parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); + var passesBeforeReconnect = driver.DiscoverCount; // 4 + + // Force a reconnect: Connected → Reconnecting → (auto retry-connect) → Connected again. + actor.Tell(new DriverInstanceActor.ForceReconnect()); + + // A fresh discovery pass must arrive after the reconnect — the cache is warm now, so it sees + // the stable 3-node set immediately. + var afterReconnect = parent.ExpectMsg(TimeSpan.FromSeconds(3)); + afterReconnect.Nodes.Count.ShouldBe(3); + afterReconnect.DriverInstanceId.ShouldBe(driver.DriverInstanceId); + + // The driver was discovered again — proves a fresh loop ran, not a replay of the old one. + driver.DiscoverCount.ShouldBeGreaterThan(passesBeforeReconnect); + } + /// /// A that also exposes . Each DiscoverAsync /// pass is counted; passes 1–2 yield nothing (cache warming), passes 3+ yield a stable 3-node set —