docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -23,7 +23,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests;
/// <summary>
/// WP-1/2/4/5/6/8/16: Tests for central-side DeploymentService.
/// Tests for central-side DeploymentService.
/// </summary>
public class DeploymentServiceTests : TestKit
{
@@ -34,6 +34,7 @@ public class DeploymentServiceTests : TestKit
private readonly IAuditService _audit;
private readonly DeploymentService _service;
/// <summary>Initializes a new instance of the <c>DeploymentServiceTests</c> class.</summary>
public DeploymentServiceTests()
{
_repo = Substitute.For<IDeploymentManagerRepository>();
@@ -62,7 +63,7 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// Notify-and-fetch (Task 6): the central HTTP base URL carried in the
/// Notify-and-fetch: the central HTTP base URL carried in the
/// <c>RefreshDeploymentCommand</c>. Must be non-empty for the deploy path to
/// clear the fail-fast configuration guard.
/// </summary>
@@ -86,8 +87,10 @@ public class DeploymentServiceTests : TestKit
PendingDeploymentTtl = TestPendingTtl
});
// ── WP-1: Deployment flow ──
// ── Deployment flow ──
/// <summary>Verifies that deploy instance async instance not found returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_InstanceNotFound_ReturnsFailure()
{
@@ -99,6 +102,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not found", result.Error);
}
/// <summary>Verifies that deploy instance async validation fails returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_ValidationFails_ReturnsFailure()
{
@@ -119,6 +124,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("validation failed", result.Error);
}
/// <summary>Verifies that deploy instance async flattening fails returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_FlatteningFails_ReturnsFailure()
{
@@ -134,12 +141,14 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("Validation failed", result.Error);
}
// ── DeploymentManager-021: missing Site row -> hard failure, no silent fabrication ──
// ── Missing Site row -> hard failure, no silent fabrication ──
/// <summary>Verifies that deploy instance async site row missing fails loudly instead of silently substituting.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_SiteRowMissing_FailsLoudlyInsteadOfSilentlySubstituting()
{
// DeploymentManager-021 regression: previously ResolveSiteIdentifierAsync
// Regression: Previously ResolveSiteIdentifierAsync
// silently returned the numeric siteId rendered as a string when the
// site row was missing (FK was deleted, race with admin delete, DB
// inconsistency). That bogus identifier then surfaced downstream as a
@@ -181,12 +190,14 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not found", result.Error, StringComparison.OrdinalIgnoreCase);
}
// ── DeploymentManager-022: no transient Pending -> single InProgress insert ──
// ── No transient Pending -> single InProgress insert ──
/// <summary>Verifies that deploy instance async no transient pending write record created directly in progress.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_NoTransientPendingWrite_RecordCreatedDirectlyInProgress()
{
// DeploymentManager-022 regression: previously the deploy path wrote
// Regression: Previously the deploy path wrote
// the record as Pending, then immediately updated it to InProgress
// with no work in between — an extra SaveChangesAsync round-trip, an
// extra notifier push, and a Pending->InProgress flicker in the
@@ -222,8 +233,10 @@ public class DeploymentServiceTests : TestKit
Arg.Any<CancellationToken>());
}
// ── WP-2: Deployment identity ──
// ── Deployment identity ──
/// <summary>Verifies that deploy instance async creates unique deployment id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_CreatesUniqueDeploymentId()
{
@@ -257,8 +270,10 @@ public class DeploymentServiceTests : TestKit
Assert.Equal("sha256:abc", captured.RevisionHash);
}
// ── WP-4: State transition validation ──
// ── State transition validation ──
/// <summary>Verifies that deploy instance async enabled instance allows deploy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_EnabledInstance_AllowsDeploy()
{
@@ -277,8 +292,10 @@ public class DeploymentServiceTests : TestKit
await _repo.Received().AddDeploymentRecordAsync(Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
}
// ── DeploymentManager-001: unexpected exception must not leave record InProgress ──
// ── Unexpected exception must not leave record InProgress ──
/// <summary>Verifies that deploy instance async communication throws unexpected exception record marked failed.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_CommunicationThrowsUnexpectedException_RecordMarkedFailed()
{
@@ -310,8 +327,10 @@ public class DeploymentServiceTests : TestKit
Assert.NotNull(captured.CompletedAt);
}
// ── DeploymentManager-002: failure write must not use a cancelled token ──
// ── Failure write must not use a cancelled token ──
/// <summary>Verifies that deploy instance async failure write uses non cancellable token.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_FailureWrite_UsesNonCancellableToken()
{
@@ -355,6 +374,8 @@ public class DeploymentServiceTests : TestKit
// ── AskTimeoutException: Akka's AskTimeoutException must be classified as a deploy timeout ──
/// <summary>Verifies that deploy instance async ask timeout exception classified as timeout.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_AskTimeoutException_ClassifiedAsTimeout()
{
@@ -363,7 +384,7 @@ public class DeploymentServiceTests : TestKit
// AskTimeoutException (which does NOT derive from System.TimeoutException).
// Prior to the fix this fell through to the generic "Deployment error:"
// branch instead of the timeout "Communication failure:" branch, which
// broke the DeploymentManager-006 reconciliation that keys off that prefix.
// broke the query-the-site-before-redeploy reconciliation that keys off that prefix.
var instance = new Instance("AskTimeoutInst") { Id = 99, SiteId = 1, State = InstanceState.NotDeployed };
_repo.GetInstanceByIdAsync(99, Arg.Any<CancellationToken>()).Returns(instance);
SetupValidPipeline(99, "AskTimeoutInst", "sha256:target");
@@ -403,7 +424,7 @@ public class DeploymentServiceTests : TestKit
Assert.DoesNotContain("Deployment failed:", result.Error);
// The deployment record must be Failed with the timeout-failure prefix
// (which the DeploymentManager-006 reconciliation check keys off).
// (which the query-the-site-before-redeploy reconciliation check keys off).
Assert.NotNull(captured);
Assert.Equal(DeploymentStatus.Failed, captured!.Status);
Assert.NotNull(captured.ErrorMessage);
@@ -411,8 +432,10 @@ public class DeploymentServiceTests : TestKit
Assert.DoesNotContain("Deployment error:", captured.ErrorMessage);
}
// ── WP-6: Lifecycle commands ──
// ── Lifecycle commands ──
/// <summary>Verifies that disable instance async instance not found returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisableInstanceAsync_InstanceNotFound_ReturnsFailure()
{
@@ -424,6 +447,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not found", result.Error);
}
/// <summary>Verifies that disable instance async when disabled returns transition error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisableInstanceAsync_WhenDisabled_ReturnsTransitionError()
{
@@ -436,6 +461,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not allowed", result.Error);
}
/// <summary>Verifies that enable instance async when enabled returns transition error.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task EnableInstanceAsync_WhenEnabled_ReturnsTransitionError()
{
@@ -448,6 +475,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not allowed", result.Error);
}
/// <summary>Verifies that delete instance async instance not found returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteInstanceAsync_InstanceNotFound_ReturnsFailure()
{
@@ -459,10 +488,12 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("not found", result.Error);
}
/// <summary>Verifies that delete instance async not deployed removes record without site round trip.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteInstanceAsync_NotDeployed_RemovesRecordWithoutSiteRoundTrip()
{
// #05-T18: a NotDeployed instance has no live actor at any site (Transport-
// A NotDeployed instance has no live actor at any site (Transport-
// imported instances always land NotDeployed, often against an uncommissioned
// or unreachable site). Delete must succeed as a central-side record cleanup
// WITHOUT contacting the site. The default _service's CommunicationService has
@@ -478,8 +509,10 @@ public class DeploymentServiceTests : TestKit
await _repo.Received(1).DeleteInstanceAsync(70, Arg.Any<CancellationToken>());
}
// ── DeploymentManager-004: site-success but central-delete-failure must not escape uncaught ──
// ── Site-success but central-delete-failure must not escape uncaught ──
/// <summary>Verifies that delete instance async site succeeds central delete fails returns distinct failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteInstanceAsync_SiteSucceeds_CentralDeleteFails_ReturnsDistinctFailure()
{
@@ -507,8 +540,10 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("central", result.Error, StringComparison.OrdinalIgnoreCase);
}
// ── WP-8: Deployment comparison ──
// ── Deployment comparison ──
/// <summary>Verifies that get deployment comparison async no snapshot returns failure.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_NoSnapshot_ReturnsFailure()
{
@@ -520,6 +555,8 @@ public class DeploymentServiceTests : TestKit
Assert.Contains("No deployed snapshot", result.Error);
}
/// <summary>Verifies that get deployment comparison async same hash not stale.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_SameHash_NotStale()
{
@@ -554,6 +591,8 @@ public class DeploymentServiceTests : TestKit
Assert.False(result.Value.IsStale);
}
/// <summary>Verifies that get deployment comparison async different hash is stale.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_DifferentHash_IsStale()
{
@@ -591,8 +630,10 @@ public class DeploymentServiceTests : TestKit
Assert.True(result.Value.IsStale);
}
// ── PLAN-05 Task 16: read-only paths skip Roslyn script compilation ──
// ── Read-only paths skip Roslyn script compilation ──
/// <summary>Verifies that get deployment comparison async requests validate scripts false.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_RequestsValidateScriptsFalse()
{
@@ -622,6 +663,8 @@ public class DeploymentServiceTests : TestKit
await _pipeline.DidNotReceive().FlattenAndValidateAsync(1, Arg.Any<CancellationToken>(), true);
}
/// <summary>Verifies that deploy instance async requests validate scripts true.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_RequestsValidateScriptsTrue()
{
@@ -642,6 +685,8 @@ public class DeploymentServiceTests : TestKit
await _pipeline.Received().FlattenAndValidateAsync(1, Arg.Any<CancellationToken>(), true);
}
/// <summary>Verifies that stale instance probe requests validate scripts false.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task StaleInstanceProbe_RequestsValidateScriptsFalse()
{
@@ -663,8 +708,10 @@ public class DeploymentServiceTests : TestKit
await _pipeline.Received(1).FlattenAndValidateAsync(5, Arg.Any<CancellationToken>(), false, Arg.Any<FlattenSession?>());
}
// ── DeploymentManager-007: comparison must produce a structured diff ──
// ── Comparison must produce a structured diff ──
/// <summary>Verifies that get deployment comparison async produces structured diff.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_ProducesStructuredDiff()
{
@@ -710,6 +757,8 @@ public class DeploymentServiceTests : TestKit
// ── I-1 (latent): old-form List snapshot must not show a spurious stale/diff ──
/// <summary>Verifies that get deployment comparison async old form list value normalized to native not stale no diff.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_OldFormListValue_NormalizedToNative_NotStaleNoDiff()
{
@@ -785,6 +834,8 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(nativeHash, result.Value.DeployedRevisionHash);
}
/// <summary>Verifies that get deployment comparison async genuinely changed list value still stale and diffs.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentComparisonAsync_GenuinelyChangedListValue_StillStaleAndDiffs()
{
@@ -845,8 +896,10 @@ public class DeploymentServiceTests : TestKit
c => c.CanonicalName == "Thresholds" && c.ChangeType == DiffChangeType.Changed);
}
// ── WP-2: GetDeploymentStatusAsync ──
// ── GetDeploymentStatusAsync ──
/// <summary>Verifies that get deployment status async returns record by deployment id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task GetDeploymentStatusAsync_ReturnsRecordByDeploymentId()
{
@@ -865,10 +918,12 @@ public class DeploymentServiceTests : TestKit
// ── Audit logging ──
/// <summary>Verifies that deploy instance async flattening fails does not reach audit.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_FlatteningFails_DoesNotReachAudit()
{
// DeploymentManager-011: this test previously asserted nothing. A
// This test previously asserted nothing. A
// flatten failure returns before any site communication, so no audit
// entry is written.
var instance = new Instance("TestInst") { Id = 1, SiteId = 1, State = InstanceState.NotDeployed };
@@ -884,10 +939,12 @@ public class DeploymentServiceTests : TestKit
Arg.Any<string>(), Arg.Any<object>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async site succeeds writes deploy audit entry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_SiteSucceeds_WritesDeployAuditEntry()
{
// DeploymentManager-011: a successful deployment must write a "Deploy"
// A successful deployment must write a "Deploy"
// audit entry referencing the deployed instance.
var instance = new Instance("AuditInst") { Id = 50, SiteId = 1, State = InstanceState.NotDeployed };
_repo.GetInstanceByIdAsync(50, Arg.Any<CancellationToken>()).Returns(instance);
@@ -908,10 +965,12 @@ public class DeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async deploy audit write throws keeps committed success.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_DeployAuditWriteThrows_KeepsCommittedSuccess()
{
// #05-T17: the post-success "Deploy" audit write is best-effort. If it
// The post-success "Deploy" audit write is best-effort. If it
// throws, the already-committed Success record must NOT be flipped to
// Failed by the outer catch — "audit-write failure NEVER aborts the
// user-facing action".
@@ -940,6 +999,8 @@ public class DeploymentServiceTests : TestKit
// ── Telemetry follow-on: scadabridge.deployments.applied on deploy success ──
/// <summary>Verifies that deploy instance async site succeeds emits deployments applied counter once.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_SiteSucceeds_EmitsDeploymentsAppliedCounterOnce()
{
@@ -983,6 +1044,8 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, Interlocked.Read(ref applied));
}
/// <summary>Verifies that deploy instance async reconciled does not emit deployments applied counter.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Reconciled_DoesNotEmitDeploymentsAppliedCounter()
{
@@ -1038,8 +1101,10 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(0, Interlocked.Read(ref applied));
}
// ── DeploymentManager-011: lifecycle success paths ──
// ── Lifecycle success paths ──
/// <summary>Verifies that disable instance async site succeeds sets disabled state and audits.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisableInstanceAsync_SiteSucceeds_SetsDisabledStateAndAudits()
{
@@ -1061,6 +1126,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that enable instance async site succeeds sets enabled state and audits.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task EnableInstanceAsync_SiteSucceeds_SetsEnabledStateAndAudits()
{
@@ -1082,6 +1149,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that delete instance async site succeeds removes record and audits.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteInstanceAsync_SiteSucceeds_RemovesRecordAndAudits()
{
@@ -1102,10 +1171,12 @@ public class DeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async same instance operation lock serializes concurrent deploys.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_SameInstance_OperationLockSerializesConcurrentDeploys()
{
// DeploymentManager-011: two concurrent deploys of the SAME instance
// Two concurrent deploys of the SAME instance
// must be serialized by the per-instance operation lock — the site sees
// them one at a time, never overlapping.
var instance = new Instance("LockInst") { Id = 54, SiteId = 1, State = InstanceState.Enabled };
@@ -1130,7 +1201,7 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, serializationCounters.MaxConcurrent);
}
// ── DeploymentManager-006: query-the-site-before-redeploy idempotency ──
// ── Query-the-site-before-redeploy idempotency ──
/// <summary>
/// Builds a DeploymentService whose CommunicationService is backed by the
@@ -1172,11 +1243,11 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// DeploymentManager-021 test helper: returns an <see cref="ISiteRepository"/>
/// Test helper: Returns an <see cref="ISiteRepository"/>
/// substitute that resolves <see cref="ISiteRepository.GetSiteByIdAsync"/>
/// for ANY integer id to a stub <see cref="Site"/> whose
/// <c>SiteIdentifier</c> is <c>"site-{id}"</c>. Prior to the
/// DeploymentManager-021 fix the production `ResolveSiteIdentifierAsync`
/// missing-site-row fix the production `ResolveSiteIdentifierAsync`
/// silently substituted the numeric id when the site row was missing, so
/// these tests passed without seeding any Sites. After the fix a missing
/// site throws — every test that drives a deploy/lifecycle path needs a
@@ -1195,6 +1266,8 @@ public class DeploymentServiceTests : TestKit
return siteRepo;
}
/// <summary>Verifies that deploy instance async prior in progress record site has target hash marks success without redeploy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_PriorInProgressRecord_SiteHasTargetHash_MarksSuccessWithoutRedeploy()
{
@@ -1230,6 +1303,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async prior in progress record site has different hash proceeds with deploy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_PriorInProgressRecord_SiteHasDifferentHash_ProceedsWithDeploy()
{
@@ -1262,6 +1337,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async prior failed timeout record queries site.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_PriorFailedTimeoutRecord_QueriesSite()
{
@@ -1292,6 +1369,8 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(DeploymentStatus.Success, prior.Status);
}
/// <summary>Verifies that deploy instance async prior success record skips site query.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_PriorSuccessRecord_SkipsSiteQuery()
{
@@ -1321,6 +1400,8 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, counters.DeployCount);
}
/// <summary>Verifies that deploy instance async fresh first time deploy skips site query.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_FreshFirstTimeDeploy_SkipsSiteQuery()
{
@@ -1343,6 +1424,8 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, counters.DeployCount);
}
/// <summary>Verifies that deploy instance async prior in progress record query fails falls through to deploy.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_PriorInProgressRecord_QueryFails_FallsThroughToDeploy()
{
@@ -1375,8 +1458,10 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, counters.DeployCount);
}
// ── Task 6: notify-and-fetch send path (stage PendingDeployment + send RefreshDeploymentCommand) ──
// ── Notify-and-fetch send path (stage PendingDeployment + send RefreshDeploymentCommand) ──
/// <summary>Verifies that deploy instance async success stages pending config then sends refresh notify.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Success_StagesPendingConfigThenSendsRefreshNotify()
{
@@ -1433,6 +1518,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<PendingDeployment>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async success does not delete pending row.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Success_DoesNotDeletePendingRow()
{
@@ -1461,6 +1548,8 @@ public class DeploymentServiceTests : TestKit
Arg.Any<string>(), Arg.Any<CancellationToken>());
}
/// <summary>Verifies that deploy instance async central fetch base url empty fails fast without notify or staging.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_CentralFetchBaseUrlEmpty_FailsFastWithoutNotifyOrStaging()
{
@@ -1496,8 +1585,10 @@ public class DeploymentServiceTests : TestKit
Arg.Any<DeploymentRecord>(), Arg.Any<CancellationToken>());
}
// ── DeploymentManager-015: reconciliation must perform the normal success side effects ──
// ── Reconciliation must perform the normal success side effects ──
/// <summary>Verifies that deploy instance async reconciled sets instance enabled and stores snapshot.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Reconciled_SetsInstanceEnabledAndStoresSnapshot()
{
@@ -1539,24 +1630,26 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
// DeploymentManager-015: the instance State must reflect the deployed
// The instance State must reflect the deployed
// config the site is actually running.
Assert.Equal(InstanceState.Enabled, instance.State);
await _repo.Received().UpdateInstanceAsync(instance, Arg.Any<CancellationToken>());
// DeploymentManager-015: a deployed-config snapshot must be stored so
// A deployed-config snapshot must be stored so
// GetDeploymentComparisonAsync has something to compare against.
Assert.NotNull(storedSnapshot);
Assert.Equal(70, storedSnapshot!.InstanceId);
Assert.Equal("sha256:target", storedSnapshot.RevisionHash);
}
// ── DeploymentManager-018: reconciliation must preserve an intentional Disabled state ──
// ── Reconciliation must preserve an intentional Disabled state ──
/// <summary>Verifies that deploy instance async reconciled disabled instance preserves disabled state.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Reconciled_DisabledInstance_PreservesDisabledState()
{
// DeploymentManager-018: after a central failover, the in-memory
// After a central failover, the in-memory
// OperationLockManager is lost (by design — in-progress treated as
// failed). The prior deployment record remains InProgress in the DB.
// The operator can legitimately invoke Disable on the instance between
@@ -1596,19 +1689,20 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(1, counters.QueryCount);
Assert.Equal(0, counters.DeployCount);
// DeploymentManager-018: the operator's explicit Disable must survive
// The operator's explicit Disable must survive
// the reconciliation — Instance.State stays Disabled, not silently
// flipped to Enabled.
Assert.Equal(InstanceState.Disabled, instance.State);
}
/// <summary>
/// The counterpart to the bulk path's Disabled skip (arch-review P7-2): a
/// The counterpart to the bulk path's Disabled skip: a
/// SINGLE-instance deploy of a Disabled instance still deploys AND enables it.
/// That is the documented "Deploy on a Disabled instance also enables it"
/// transition and the explicit operator action bulk deploy now refuses to
/// perform implicitly, so it must keep working exactly as before.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_DisabledInstance_StillDeploysAndEnables()
{
@@ -1632,8 +1726,10 @@ public class DeploymentServiceTests : TestKit
Assert.Equal(InstanceState.Enabled, instance.State);
}
// ── DeploymentManager-016: reconciled record must carry the target revision hash ──
// ── Reconciled record must carry the target revision hash ──
/// <summary>Verifies that deploy instance async reconciled prior record revision hash updated to target.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Reconciled_PriorRecordRevisionHashUpdatedToTarget()
{
@@ -1667,17 +1763,19 @@ public class DeploymentServiceTests : TestKit
Assert.True(result.IsSuccess);
Assert.Equal(DeploymentStatus.Success, prior.Status);
// DeploymentManager-016: the persisted record's RevisionHash must agree
// The persisted record's RevisionHash must agree
// with the site's applied revision, not keep the stale R1.
Assert.Equal("sha256:target", prior.RevisionHash);
}
// ── DeploymentManager-020: reconciliation audit attributes to the CURRENT user, not the prior deployer ──
// ── Reconciliation audit attributes to the CURRENT user, not the prior deployer ──
/// <summary>Verifies that deploy instance async reconciled audit attributes current user not prior deployer.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_Reconciled_AuditAttributesCurrentUserNotPriorDeployer()
{
// DeploymentManager-020: a redeploy that reconciles a timed-out prior
// A redeploy that reconciles a timed-out prior
// record must be audited as the action of the user driving THIS
// redeploy — not the user who originally issued the now-reconciled
// deployment. The prior deployer is preserved in the detail object so
@@ -1707,7 +1805,7 @@ public class DeploymentServiceTests : TestKit
var result = await service.DeployInstanceAsync(73, "currentUser");
Assert.True(result.IsSuccess);
// DeploymentManager-020: audit row's actor is the current user.
// Audit row's actor is the current user.
await _audit.Received().LogAsync(
"currentUser", "DeployReconciled", "Instance", "73", "ReconcileAuditUser",
Arg.Any<object>(), Arg.Any<CancellationToken>());
@@ -1717,8 +1815,10 @@ public class DeploymentServiceTests : TestKit
Arg.Any<object>(), Arg.Any<CancellationToken>());
}
// ── DeploymentManager-012: LifecycleCommandTimeout must actually bound lifecycle commands ──
// ── LifecycleCommandTimeout must actually bound lifecycle commands ──
/// <summary>Verifies that disable instance async site unresponsive lifecycle command timeout bounds the wait.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisableInstanceAsync_SiteUnresponsive_LifecycleCommandTimeoutBoundsTheWait()
{
@@ -1771,17 +1871,19 @@ public class DeploymentServiceTests : TestKit
/// <summary>Stand-in actor that never replies to anything.</summary>
private class SilentProbeActor : ReceiveActor
{
/// <summary>Initializes a new instance of the <c>SilentProbeActor</c> class.</summary>
public SilentProbeActor() => ReceiveAny(_ => { });
}
// ── DeploymentManager-019: lifecycle timeouts must write an audit entry ──
// ── Lifecycle timeouts must write an audit entry ──
/// <summary>
/// DeploymentManager-019: when a Disable times out at the site, the
/// When a Disable times out at the site, the
/// operator's attempted action must still be recorded in the audit log
/// with the documented <c>DisableTimedOut</c> action — pre-fix nothing
/// was written and the audit trail was silent about the attempt.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DisableInstanceAsync_LifecycleTimeout_WritesDisableTimedOutAuditEntry()
{
@@ -1833,8 +1935,9 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// DeploymentManager-019: same audit guarantee for the Enable path.
/// Same audit guarantee for the Enable path.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task EnableInstanceAsync_LifecycleTimeout_WritesEnableTimedOutAuditEntry()
{
@@ -1881,8 +1984,9 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// DeploymentManager-019: same audit guarantee for the Delete path.
/// Same audit guarantee for the Delete path.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeleteInstanceAsync_LifecycleTimeout_WritesDeleteTimedOutAuditEntry()
{
@@ -1928,8 +2032,10 @@ public class DeploymentServiceTests : TestKit
Arg.Any<CancellationToken>());
}
// ── DeploymentManager-003: post-success persistence must commit the Success status ──
// ── Post-success persistence must commit the Success status ──
/// <summary>Verifies that deploy instance async site succeeds snapshot write fails record still committed success.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DeployInstanceAsync_SiteSucceeds_SnapshotWriteFails_RecordStillCommittedSuccess()
{
@@ -1974,8 +2080,8 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// Per-test counters for <see cref="SerializationProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance, passed into the actor's constructor, so
/// Per-test counters for <see cref="SerializationProbeActor"/>.
/// 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>
@@ -1992,13 +2098,18 @@ public class DeploymentServiceTests : TestKit
/// same instance were NOT serialized by the operation lock their windows
/// would overlap and <c>MaxConcurrent</c> would exceed 1.
///
/// Notify-and-fetch (Task 6): the deploy path now sends a
/// Notify-and-fetch: the deploy path now sends a
/// <see cref="RefreshDeploymentCommand"/> instead of <c>DeployInstanceCommand</c>.
/// </summary>
private class SerializationProbeActor : ReceiveActor, IWithTimers
{
/// <summary>Gets or sets the actor timer scheduler supplied by Akka.</summary>
public ITimerScheduler Timers { get; set; } = null!;
/// <summary>
/// Initializes a new instance of the <c>SerializationProbeActor</c> class.
/// </summary>
/// <param name="counters">The counters.</param>
public SerializationProbeActor(SerializationProbeCounters counters)
{
Receive<SiteEnvelope>(env =>
@@ -2043,8 +2154,8 @@ public class DeploymentServiceTests : TestKit
}
/// <summary>
/// Per-test counters for <see cref="ReconcileProbeActor"/>. DeploymentManager-024:
/// each test owns its own instance so counter assertions cannot race across
/// Per-test counters for <see cref="ReconcileProbeActor"/>.
/// Each test owns its own instance so counter assertions cannot race across
/// tests running in parallel.
/// </summary>
private sealed class ReconcileProbeCounters
@@ -2053,7 +2164,7 @@ public class DeploymentServiceTests : TestKit
public int DeployCount;
/// <summary>
/// Notify-and-fetch (Task 6): the most recent
/// Notify-and-fetch: the most recent
/// <see cref="RefreshDeploymentCommand"/> the probe received, so a test
/// can assert the deploy path notified the site with the staged token,
/// fetch base URL, and deployment id.
@@ -2068,13 +2179,19 @@ public class DeploymentServiceTests : TestKit
/// configurable applied revision hash, and (optionally) drops the query to
/// simulate an unreachable site so the central Ask times out.
///
/// Notify-and-fetch (Task 6): the deploy path now sends a
/// Notify-and-fetch: the deploy path now sends a
/// <see cref="RefreshDeploymentCommand"/> (not the fat
/// <c>DeployInstanceCommand</c>), so the probe counts that message as a
/// "deploy" and records it for assertion.
/// </summary>
private class ReconcileProbeActor : ReceiveActor
{
/// <summary>
/// Initializes a new instance of the <c>ReconcileProbeActor</c> class.
/// </summary>
/// <param name="counters">The counters.</param>
/// <param name="siteHash">The site hash.</param>
/// <param name="failQuery">The fail query.</param>
public ReconcileProbeActor(ReconcileProbeCounters counters, string siteHash, bool failQuery)
{
Receive<SiteEnvelope>(env =>