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:
@@ -391,6 +391,121 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
Assert.Equal(3, allIds.Count);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task SiteCallQueryRequest_StuckOnly_CursorAtNonStuckBoundary_SkipsToNextStuckRow()
|
||||
{
|
||||
// SiteCallAudit-006 boundary regression. The earlier paging test
|
||||
// interleaves stuck/non-stuck rows but the cursor between page-1 and
|
||||
// page-2 always lands on a stuck row. This test forces the cursor to
|
||||
// sit on a NON-stuck row (page-size=1 over a strict
|
||||
// stuck-not_stuck-stuck-not_stuck-stuck-not_stuck pattern oldest-first)
|
||||
// so the SQL-side composition of the stuck predicate AND the keyset
|
||||
// cursor predicate (CreatedAtUtc < cursor OR =cursor AND id < ...) must
|
||||
// honestly skip the non-stuck rows between each page. Each page must
|
||||
// return exactly one stuck row, with no overlap and all three stuck
|
||||
// rows visited across three pages.
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
var siteId = NewSiteId();
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
var actor = CreateActor(repo, new SiteCallAuditOptions { StuckAgeThreshold = TimeSpan.FromMinutes(10) });
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var stuckIds = new List<Guid>();
|
||||
// Insert pattern (relative-newest first, the order the actor returns
|
||||
// them in DESC-by-CreatedAtUtc):
|
||||
// t=-1m (non-stuck — Attempted but only 1m old, < 10m threshold),
|
||||
// t=-15m (stuck — Attempted, 15m > 10m: stuckA),
|
||||
// t=-20m (non-stuck — terminal Delivered between stuckA & stuckB),
|
||||
// t=-25m (stuck — Attempted, 25m > 10m: stuckB),
|
||||
// t=-30m (non-stuck — terminal Delivered between stuckB & stuckC),
|
||||
// t=-35m (stuck — Attempted, 35m > 10m: stuckC),
|
||||
// t=-40m (non-stuck — terminal Delivered, oldest of all).
|
||||
// The non-stuck rows at -20m and -30m sit between consecutive stuck
|
||||
// rows, so the page-size-1 cursor lands ON a non-stuck row between
|
||||
// pages — exactly the boundary the predicate composition must skip.
|
||||
var stuckA = TrackedOperationId.New();
|
||||
var stuckB = TrackedOperationId.New();
|
||||
var stuckC = TrackedOperationId.New();
|
||||
// Expected DESC-by-CreatedAtUtc order: A (-15m newest), B (-25m), C (-35m oldest).
|
||||
stuckIds.Add(stuckA.Value);
|
||||
stuckIds.Add(stuckB.Value);
|
||||
stuckIds.Add(stuckC.Value);
|
||||
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Delivered",
|
||||
createdAtUtc: now.AddMinutes(-40), terminal: true));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
stuckC, siteId, status: "Attempted",
|
||||
createdAtUtc: now.AddMinutes(-35)));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Delivered",
|
||||
createdAtUtc: now.AddMinutes(-30), terminal: true));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
stuckB, siteId, status: "Attempted",
|
||||
createdAtUtc: now.AddMinutes(-25)));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Delivered",
|
||||
createdAtUtc: now.AddMinutes(-20), terminal: true));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
stuckA, siteId, status: "Attempted",
|
||||
createdAtUtc: now.AddMinutes(-15)));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Attempted",
|
||||
createdAtUtc: now.AddMinutes(-1)));
|
||||
|
||||
// Page-1: page-size=1, expect stuckA (newest stuck row).
|
||||
actor.Tell(
|
||||
new SiteCallQueryRequest(
|
||||
"corr-stuck-b1", null, siteId, null, null, StuckOnly: true,
|
||||
null, null, null, null, PageSize: 1),
|
||||
TestActor);
|
||||
var page1 = ExpectMsg<SiteCallQueryResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(page1.Success);
|
||||
Assert.Single(page1.SiteCalls);
|
||||
Assert.True(page1.SiteCalls[0].IsStuck);
|
||||
Assert.Equal(stuckA.Value, page1.SiteCalls[0].TrackedOperationId);
|
||||
Assert.NotNull(page1.NextAfterCreatedAtUtc);
|
||||
|
||||
// Page-2: between stuckA and stuckB the non-stuck terminal row at -20m
|
||||
// sits at the cursor — the SQL must skip it, NOT return it.
|
||||
actor.Tell(
|
||||
new SiteCallQueryRequest(
|
||||
"corr-stuck-b2", null, siteId, null, null, StuckOnly: true,
|
||||
null, null, page1.NextAfterCreatedAtUtc, page1.NextAfterId,
|
||||
PageSize: 1),
|
||||
TestActor);
|
||||
var page2 = ExpectMsg<SiteCallQueryResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(page2.Success);
|
||||
Assert.Single(page2.SiteCalls);
|
||||
Assert.True(page2.SiteCalls[0].IsStuck);
|
||||
Assert.Equal(stuckB.Value, page2.SiteCalls[0].TrackedOperationId);
|
||||
Assert.NotNull(page2.NextAfterCreatedAtUtc);
|
||||
|
||||
// Page-3: between stuckB and stuckC the non-stuck row at -30m sits at
|
||||
// the cursor — again must be skipped.
|
||||
actor.Tell(
|
||||
new SiteCallQueryRequest(
|
||||
"corr-stuck-b3", null, siteId, null, null, StuckOnly: true,
|
||||
null, null, page2.NextAfterCreatedAtUtc, page2.NextAfterId,
|
||||
PageSize: 1),
|
||||
TestActor);
|
||||
var page3 = ExpectMsg<SiteCallQueryResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(page3.Success);
|
||||
Assert.Single(page3.SiteCalls);
|
||||
Assert.True(page3.SiteCalls[0].IsStuck);
|
||||
Assert.Equal(stuckC.Value, page3.SiteCalls[0].TrackedOperationId);
|
||||
|
||||
// All three stuck rows visited exactly once, in DESC order, with no
|
||||
// non-stuck rows leaking through despite the cursor sitting on them.
|
||||
var visited = new[] { page1, page2, page3 }
|
||||
.SelectMany(p => p.SiteCalls)
|
||||
.Select(s => s.TrackedOperationId)
|
||||
.ToList();
|
||||
Assert.Equal(stuckIds, visited);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task SiteCallDetailRequest_KnownId_ReturnsFullDetail()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user