diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/ReconcileStartsInstanceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/ReconcileStartsInstanceTests.cs
index 13788bd7..d9cd34bc 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/ReconcileStartsInstanceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Actors/ReconcileStartsInstanceTests.cs
@@ -68,20 +68,49 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
TestLocalDb.DeleteFiles(path);
}
- private IActorRef CreateDeploymentManager(IServiceProvider? serviceProvider = null) =>
- ActorOf(Props.Create(() => new DeploymentManagerActor(
+ ///
+ /// Starts a Deployment Manager and blocks until its boot-time config load has PROVABLY read
+ /// storage — its first ISiteHealthCollector.SetInstanceCounts report.
+ ///
+ ///
+ /// Deferred register row 83. This used to be a bare Task.Delay(500). Every test
+ /// here writes its deployed_configurations row AFTER the manager is constructed, and a
+ /// row that lands before the boot load reads storage is picked up by that load — so the test
+ /// would still pass, while proving the BOOT LOAD works rather than the startup-reconcile path
+ /// it claims to cover. A sleep only makes that race unlikely; on a loaded machine the load can
+ /// still be in flight when it expires, and the test silently changes what it proves instead of
+ /// failing. The row-78 fix session hit exactly this trap and closed it in
+ /// InstanceSweepSelfHealTests; this is the same gate, applied to row 67's suite.
+ ///
+ ///
+ ///
+ /// The sweep is disabled (InstanceSweepIntervalSeconds = 0) so these tests keep proving
+ /// the row-67 path specifically — with the sweep
+ /// armed, its periodic self-heal would independently start the same instances and the suite
+ /// would no longer distinguish the two mechanisms.
+ ///
+ ///
+ private IActorRef CreateDeploymentManager(IServiceProvider? serviceProvider = null)
+ {
+ var health = new DeploymentManagerStartupProbe();
+ var dm = ActorOf(Props.Create(() => new DeploymentManagerActor(
_storage,
_compilationService,
_sharedScriptLibrary,
null,
- new SiteRuntimeOptions(),
+ new SiteRuntimeOptions { InstanceSweepIntervalSeconds = 0 },
NullLogger.Instance,
dclManager: null,
- healthCollector: null,
+ healthCollector: health,
serviceProvider: serviceProvider,
loggerFactory: null,
configFetcher: null)));
+ AwaitCondition(() => health.ReportCount > 0, TimeSpan.FromSeconds(20),
+ TimeSpan.FromMilliseconds(25));
+ return dm;
+ }
+
private IActorRef CreateReconciliationActor(
IActorRef siteCommunicationActor,
IDeploymentConfigFetcher fetcher,
@@ -143,7 +172,6 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
// Boot the Deployment Manager against EMPTY storage — this is the missing-config boot
// (lost volume / first boot after a restore / rehomed data dir).
var dm = CreateDeploymentManager();
- await Task.Delay(500);
// Baseline: the instance is genuinely not running.
Assert.False(await InstanceIsRunningAsync(dm, "HealedPump"));
@@ -188,7 +216,6 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
{
var siteLog = new FakeSiteEventLogger();
var dm = CreateDeploymentManager(new SingleServiceProvider(siteLog));
- await Task.Delay(500);
const string instanceName = "SurfacedPump";
await _storage.StoreDeployedConfigAsync(
@@ -286,18 +313,21 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
public async Task EnsureInstanceStarted_IsIdempotent_WhenAlreadyRunning()
{
var dm = CreateDeploymentManager();
- await Task.Delay(500);
const string instanceName = "IdempotentPump";
dm.Tell(new DeployInstanceCommand(
"dep-idem", instanceName, "sha256:abc",
MakeConfigJson(instanceName), "admin", DateTimeOffset.UtcNow));
ExpectMsg(TimeSpan.FromSeconds(5));
- await Task.Delay(1000);
+
+ // Wait on the instance actually being routable, not on the clock.
+ await AwaitAssertAsync(async () =>
+ Assert.True(await InstanceIsRunningAsync(dm, instanceName)), TimeSpan.FromSeconds(10));
var before = await ResolveInstanceChildAsync(dm, instanceName);
- // Two redundant ensure-started requests, as the two nodes would send.
+ // Two redundant ensure-started requests, as the two nodes would send. This one IS a
+ // settle wait: the assertion below is that nothing happened, which cannot be awaited on.
dm.Tell(new EnsureInstanceStartedCommand(instanceName, "startup reconcile"));
dm.Tell(new EnsureInstanceStartedCommand(instanceName, "startup reconcile"));
await Task.Delay(1000);
@@ -317,7 +347,6 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
public async Task EnsureInstanceStarted_DisabledInstance_StaysStopped()
{
var dm = CreateDeploymentManager();
- await Task.Delay(500);
const string instanceName = "DisabledPump";
await _storage.StoreDeployedConfigAsync(
@@ -338,7 +367,6 @@ public class ReconcileStartsInstanceTests : TestKit, IDisposable
public async Task EnsureInstanceStarted_UnknownInstance_IsAQuietNoOp()
{
var dm = CreateDeploymentManager();
- await Task.Delay(500);
dm.Tell(new EnsureInstanceStartedCommand("NeverHeardOf", "startup reconcile"));
await Task.Delay(1500);