test(coverage): close Theme 8 — 13 test-coverage findings, +35 tests

13 well-bounded test-coverage gaps closed across 11 test projects.
Net +35 regression tests; no production code changes except the
SiteEventLogger src reference unchanged (W3 redacted only test code).

Test additions:
- CLI-022: CommandTreeTests pinned-count assertion bumped 14→16 and
  3 InlineData rows added for the audit + bundle command groups.
- Commons-020: new TransportRecordsTests covers BundleManifest /
  ExportSelection / ImportPreview / ImportResolution / ImportResult —
  ctor + System.Text.Json round-trip + record-equality (14 tests).
- CD-024: SPLIT-RANGE failure-continuation now under
  EnsureLookahead_SecondSplitThrows_LoopAborts_FirstBoundaryStillCommitted
  (Skippable MS-SQL fixture); production-shape rowversion delete
  asserted by DeleteDeploymentRecord_CurrentRowVersion_StubAttachPath_DeleteSucceeds.
- CentralUI-033: new QueryStringDrillInTests with 4 bUnit cases for
  Transport + SiteCalls drill-in / query-string handling.
- DM-024: probe actors (ReconcileProbeActor, SerializationProbeActor,
  ArtifactProbeActor) refactored from static fields to per-test instances
  (Interlocked on counter) — all 31 callers updated; no production
  changes required.
- HM-022: real-time PeriodicTimer test flake fixed by replacing
  fixed-budget Task.Delay with a RunLoopUntil poll-until-condition
  helper (5s/25ms). Production loop untouched.
- InboundAPI-023: new EndpointExtensionsTests covers the
  POST /api/{methodName} composition wiring via TestServer (7 cases:
  happy path, missing key 401, unknown method 403, invalid JSON 400,
  missing param 400, script-throws 500 sanitised, AuditActorItemKey
  stash invariant).
- MgmtSvc-021: 6 new ManagementActorTests cover the Transport bundle
  handlers (role gate for Export/Preview/Import, unknown-name
  ManagementCommandException, blocker-rejection, dedupe last-write-wins).
- SCA-006: SiteCallQueryRequest_StuckOnly_CursorAtNonStuckBoundary_SkipsToNextStuckRow
  pins the missing boundary case.
- SEL-023: stress-test `bool stop` promoted to `volatile bool` for
  cross-thread visibility under release/JIT.

Verify-only resolutions:
- NS-024: closed by NS-019 (commit ac96b83 deletion of
  NotificationDeliveryService + its test file). No edits needed.
- NotifOutbox-008: FallbackMaxRetries/FallbackRetryDelay are private
  forward-compat constants returned only when no SMTP-config row exists
  (in which case EmailNotificationDeliveryAdapter returns Permanent,
  bypassing the values entirely). Marked Resolved with note.
- Transport-010: Overwrite child-collection sync covered by the T-001/
  T-002 tests added in commit e3ca9af; per-IP throttle by
  BundleUnlockRateLimiterTests; failed-session retention by
  BundleSessionStoreTests; T-009 closed structurally via AsyncLocal.
  Marked Resolved by reference.

Build clean; all 11 affected test suites green. README regenerated:
33 open (was 46).
This commit is contained in:
Joseph Doherty
2026-05-28 08:21:03 -04:00
parent 46cb6965ac
commit d190345ef0
26 changed files with 1725 additions and 155 deletions
@@ -90,13 +90,14 @@ public class ArtifactDeploymentServiceTests : TestKit
new("Site Two", "site-2") { Id = 2 }
};
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
var service = CreateServiceWithCommActor(probe);
var result = await service.DeployToAllSitesAsync("admin");
Assert.True(result.IsSuccess);
var commands = ArtifactProbeActor.Received;
var commands = recorder.Received;
Assert.Equal(2, commands.Count);
// All per-site commands carry one shared id, equal to the summary id.
@@ -128,7 +129,8 @@ public class ArtifactDeploymentServiceTests : TestKit
new("Site Two", "fail-site") { Id = 2 }
};
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor("fail-site")));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder, "fail-site")));
var service = CreateServiceWithCommActor(probe);
var result = await service.DeployToAllSitesAsync("admin");
@@ -157,7 +159,8 @@ public class ArtifactDeploymentServiceTests : TestKit
new("Site Three", "site-3") { Id = 3 },
};
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
var service = CreateServiceWithCommActor(probe);
var result = await service.DeployToAllSitesAsync("admin");
@@ -184,7 +187,8 @@ public class ArtifactDeploymentServiceTests : TestKit
// global query is issued exactly once. Pin this so a future refactor cannot
// accidentally route RetryForSiteAsync through the multi-site loop and lose
// the audit row's deploymentId guarantee.
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
var service = CreateServiceWithCommActor(probe);
var result = await service.RetryForSiteAsync(1, "retry-site", "admin");
@@ -200,7 +204,8 @@ public class ArtifactDeploymentServiceTests : TestKit
[Fact]
public async Task RetryForSiteAsync_SiteSucceeds_ReturnsSuccessAndAudits()
{
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor()));
var recorder = new ArtifactProbeRecorder();
var probe = Sys.ActorOf(Props.Create(() => new ArtifactProbeActor(recorder)));
var service = CreateServiceWithCommActor(probe);
var result = await service.RetryForSiteAsync(1, "retry-site", "admin");
@@ -245,25 +250,34 @@ public class ArtifactDeploymentServiceTests : TestKit
NullLogger<ArtifactDeploymentService>.Instance);
}
/// <summary>
/// Per-test recorder for <see cref="ArtifactProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance, passed into the actor's constructor, so
/// the received-command list is no longer shared static state that races
/// under parallel test execution.
/// </summary>
private sealed class ArtifactProbeRecorder
{
public readonly ConcurrentBag<DeployArtifactsCommand> Received = new();
}
/// <summary>
/// Stand-in CentralCommunicationActor for artifact deployment. Records every
/// <see cref="DeployArtifactsCommand"/> it receives and replies success
/// unless the target site id is in the configured failure set.
/// <see cref="DeployArtifactsCommand"/> it receives into the per-test
/// <see cref="ArtifactProbeRecorder"/> and replies success unless the target
/// site id is in the configured failure set.
/// </summary>
private class ArtifactProbeActor : ReceiveActor
{
public static readonly ConcurrentBag<DeployArtifactsCommand> Received = new();
public ArtifactProbeActor(params string[] failingSites)
public ArtifactProbeActor(ArtifactProbeRecorder recorder, params string[] failingSites)
{
Received.Clear();
var failSet = new HashSet<string>(failingSites);
Receive<SiteEnvelope>(env =>
{
if (env.Message is DeployArtifactsCommand cmd)
{
Received.Add(cmd);
recorder.Received.Add(cmd);
var success = !failSet.Contains(env.SiteId);
Sender.Tell(new ArtifactDeploymentResponse(
cmd.DeploymentId, env.SiteId, success,
@@ -295,8 +295,9 @@ public class DeploymentServiceTests : TestKit
_repo.DeleteInstanceAsync(30, Arg.Any<CancellationToken>())
.Returns<Task>(_ => throw new InvalidOperationException("db unavailable"));
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:x", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:x", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeleteInstanceAsync(30, "admin");
@@ -458,8 +459,9 @@ public class DeploymentServiceTests : TestKit
_repo.GetCurrentDeploymentStatusAsync(50, Arg.Any<CancellationToken>())
.Returns((DeploymentRecord?)null);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(50, "admin");
@@ -478,8 +480,9 @@ public class DeploymentServiceTests : TestKit
var instance = new Instance("DisInst") { Id = 51, SiteId = 1, State = InstanceState.Enabled };
_repo.GetInstanceByIdAsync(51, Arg.Any<CancellationToken>()).Returns(instance);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "x", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "x", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DisableInstanceAsync(51, "admin");
@@ -498,8 +501,9 @@ public class DeploymentServiceTests : TestKit
var instance = new Instance("EnInst") { Id = 52, SiteId = 1, State = InstanceState.Disabled };
_repo.GetInstanceByIdAsync(52, Arg.Any<CancellationToken>()).Returns(instance);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "x", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "x", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.EnableInstanceAsync(52, "admin");
@@ -518,8 +522,9 @@ public class DeploymentServiceTests : TestKit
var instance = new Instance("DelInst") { Id = 53, SiteId = 1, State = InstanceState.Enabled };
_repo.GetInstanceByIdAsync(53, Arg.Any<CancellationToken>()).Returns(instance);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "x", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "x", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeleteInstanceAsync(53, "admin");
@@ -543,8 +548,9 @@ public class DeploymentServiceTests : TestKit
_repo.GetCurrentDeploymentStatusAsync(54, Arg.Any<CancellationToken>())
.Returns((DeploymentRecord?)null);
var serializationCounters = new SerializationProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new SerializationProbeActor()));
new SerializationProbeActor(serializationCounters)));
var service = CreateServiceWithCommActor(commActor);
var deploy1 = service.DeployInstanceAsync(54, "admin");
@@ -555,7 +561,7 @@ public class DeploymentServiceTests : TestKit
Assert.True(results[1].IsSuccess);
// The probe records the maximum concurrency observed; the lock must
// keep it at 1 for a single instance.
Assert.Equal(1, SerializationProbeActor.MaxConcurrent);
Assert.Equal(1, serializationCounters.MaxConcurrent);
}
// ── DeploymentManager-006: query-the-site-before-redeploy idempotency ──
@@ -610,8 +616,9 @@ public class DeploymentServiceTests : TestKit
};
_repo.GetCurrentDeploymentStatusAsync(7, Arg.Any<CancellationToken>()).Returns(prior);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(7, "admin");
@@ -619,8 +626,8 @@ public class DeploymentServiceTests : TestKit
Assert.True(result.IsSuccess);
Assert.Equal(DeploymentStatus.Success, prior.Status);
// The site query was issued, but no new deploy command was sent.
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(0, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
// No new deployment record was created — the prior one was reconciled.
await _repo.DidNotReceive().AddDeploymentRecordAsync(
Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
@@ -643,16 +650,17 @@ public class DeploymentServiceTests : TestKit
};
_repo.GetCurrentDeploymentStatusAsync(8, Arg.Any<CancellationToken>()).Returns(prior);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:old", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:old", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(8, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(1, counters.QueryCount);
// The normal deploy proceeded — a new command was sent.
Assert.Equal(1, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.DeployCount);
await _repo.Received().AddDeploymentRecordAsync(
Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
}
@@ -674,15 +682,16 @@ public class DeploymentServiceTests : TestKit
};
_repo.GetCurrentDeploymentStatusAsync(9, Arg.Any<CancellationToken>()).Returns(prior);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(9, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(0, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
Assert.Equal(DeploymentStatus.Success, prior.Status);
}
@@ -702,16 +711,17 @@ public class DeploymentServiceTests : TestKit
};
_repo.GetCurrentDeploymentStatusAsync(10, Arg.Any<CancellationToken>()).Returns(prior);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(10, "admin");
Assert.True(result.IsSuccess);
// No site query — the prior deploy completed cleanly.
Assert.Equal(0, ReconcileProbeActor.QueryCount);
Assert.Equal(1, ReconcileProbeActor.DeployCount);
Assert.Equal(0, counters.QueryCount);
Assert.Equal(1, counters.DeployCount);
}
[Fact]
@@ -724,15 +734,16 @@ public class DeploymentServiceTests : TestKit
_repo.GetCurrentDeploymentStatusAsync(11, Arg.Any<CancellationToken>())
.Returns((DeploymentRecord?)null);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(11, "admin");
Assert.True(result.IsSuccess);
Assert.Equal(0, ReconcileProbeActor.QueryCount);
Assert.Equal(1, ReconcileProbeActor.DeployCount);
Assert.Equal(0, counters.QueryCount);
Assert.Equal(1, counters.DeployCount);
}
[Fact]
@@ -754,16 +765,17 @@ public class DeploymentServiceTests : TestKit
_repo.GetCurrentDeploymentStatusAsync(12, Arg.Any<CancellationToken>()).Returns(prior);
// The probe drops the query (no reply) -> the Ask times out.
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: true)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: true)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(12, "admin");
// Did not abort — the deploy proceeded after the failed query.
Assert.True(result.IsSuccess);
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(1, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.QueryCount);
Assert.Equal(1, counters.DeployCount);
}
// ── DeploymentManager-015: reconciliation must perform the normal success side effects ──
@@ -797,16 +809,17 @@ public class DeploymentServiceTests : TestKit
await _repo.AddDeployedSnapshotAsync(
Arg.Do<DeployedConfigSnapshot>(s => storedSnapshot = s), Arg.Any<CancellationToken>());
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(70, "admin");
Assert.True(result.IsSuccess);
// No re-deploy was sent -- this was reconciled.
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(0, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
// DeploymentManager-015: the instance State must reflect the deployed
// config the site is actually running.
@@ -851,8 +864,9 @@ public class DeploymentServiceTests : TestKit
_repo.GetDeployedSnapshotByInstanceIdAsync(72, Arg.Any<CancellationToken>())
.Returns((DeployedConfigSnapshot?)null);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(72, "admin");
@@ -861,8 +875,8 @@ public class DeploymentServiceTests : TestKit
// Success — central and site agree on the applied config.
Assert.True(result.IsSuccess);
Assert.Equal(DeploymentStatus.Success, prior.Status);
Assert.Equal(1, ReconcileProbeActor.QueryCount);
Assert.Equal(0, ReconcileProbeActor.DeployCount);
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
// DeploymentManager-018: the operator's explicit Disable must survive
// the reconciliation — Instance.State stays Disabled, not silently
@@ -896,8 +910,9 @@ public class DeploymentServiceTests : TestKit
_repo.GetDeployedSnapshotByInstanceIdAsync(71, Arg.Any<CancellationToken>())
.Returns((DeployedConfigSnapshot?)null);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(71, "admin");
@@ -936,8 +951,9 @@ public class DeploymentServiceTests : TestKit
_repo.GetDeployedSnapshotByInstanceIdAsync(73, Arg.Any<CancellationToken>())
.Returns((DeployedConfigSnapshot?)null);
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(73, "currentUser");
@@ -1178,8 +1194,9 @@ public class DeploymentServiceTests : TestKit
_repo.AddDeployedSnapshotAsync(Arg.Any<DeployedConfigSnapshot>(), Arg.Any<CancellationToken>())
.Returns<Task>(_ => throw new InvalidOperationException("snapshot store unavailable"));
var counters = new ReconcileProbeCounters();
var commActor = Sys.ActorOf(Props.Create(() =>
new ReconcileProbeActor(siteHash: "sha256:target", failQuery: false)));
new ReconcileProbeActor(counters, siteHash: "sha256:target", failQuery: false)));
var service = CreateServiceWithCommActor(commActor);
var result = await service.DeployInstanceAsync(20, "admin");
@@ -1196,33 +1213,40 @@ public class DeploymentServiceTests : TestKit
Arg.Any<CancellationToken>());
}
/// <summary>
/// Per-test counters for <see cref="SerializationProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance, passed into the actor's constructor, so
/// counters are no longer shared static state that races under parallel
/// test execution.
/// </summary>
private sealed class SerializationProbeCounters
{
public int MaxConcurrent;
public int Current;
public readonly object Gate = new();
}
/// <summary>
/// Stand-in CentralCommunicationActor that measures deploy concurrency. It
/// defers each deploy reply via the scheduler, so if two deploys for the
/// same instance were NOT serialized by the operation lock their windows
/// would overlap and <see cref="MaxConcurrent"/> would exceed 1.
/// would overlap and <c>MaxConcurrent</c> would exceed 1.
/// </summary>
private class SerializationProbeActor : ReceiveActor, IWithTimers
{
public static int MaxConcurrent;
private static int _current;
private static readonly object Gate = new();
public ITimerScheduler Timers { get; set; } = null!;
public SerializationProbeActor()
public SerializationProbeActor(SerializationProbeCounters counters)
{
MaxConcurrent = 0;
_current = 0;
Receive<SiteEnvelope>(env =>
{
if (env.Message is DeployInstanceCommand d)
{
lock (Gate)
lock (counters.Gate)
{
_current++;
if (_current > MaxConcurrent) MaxConcurrent = _current;
counters.Current++;
if (counters.Current > counters.MaxConcurrent)
counters.MaxConcurrent = counters.Current;
}
var replyTo = Sender;
@@ -1242,9 +1266,9 @@ public class DeploymentServiceTests : TestKit
Receive<CompleteDeploy>(c =>
{
lock (Gate)
lock (counters.Gate)
{
_current--;
counters.Current--;
}
c.ReplyTo.Tell(new DeploymentStatusResponse(
c.Command.DeploymentId, c.Command.InstanceUniqueName,
@@ -1255,29 +1279,34 @@ public class DeploymentServiceTests : TestKit
private sealed record CompleteDeploy(DeployInstanceCommand Command, IActorRef ReplyTo);
}
/// <summary>
/// Per-test counters for <see cref="ReconcileProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance so counter assertions cannot race across
/// tests running in parallel.
/// </summary>
private sealed class ReconcileProbeCounters
{
public int QueryCount;
public int DeployCount;
}
/// <summary>
/// Stand-in CentralCommunicationActor for reconciliation tests. Counts the
/// site queries and deploy commands it receives, answers queries with a
/// site queries and deploy commands it receives (into a per-test
/// <see cref="ReconcileProbeCounters"/> instance), answers queries with a
/// configurable applied revision hash, and (optionally) drops the query to
/// simulate an unreachable site so the central Ask times out.
/// </summary>
private class ReconcileProbeActor : ReceiveActor
{
public static int QueryCount;
public static int DeployCount;
public ReconcileProbeActor(string siteHash, bool failQuery)
public ReconcileProbeActor(ReconcileProbeCounters counters, string siteHash, bool failQuery)
{
// Each test creates a fresh actor; reset the shared counters.
QueryCount = 0;
DeployCount = 0;
Receive<SiteEnvelope>(env =>
{
switch (env.Message)
{
case DeploymentStateQueryRequest q:
QueryCount++;
Interlocked.Increment(ref counters.QueryCount);
if (!failQuery)
{
Sender.Tell(new DeploymentStateQueryResponse(
@@ -1288,7 +1317,7 @@ public class DeploymentServiceTests : TestKit
break;
case DeployInstanceCommand d:
DeployCount++;
Interlocked.Increment(ref counters.DeployCount);
Sender.Tell(new DeploymentStatusResponse(
d.DeploymentId, d.InstanceUniqueName,
DeploymentStatus.Success, null, DateTimeOffset.UtcNow));