fix(test): remove the unsynchronized audit-attempt assertion in the two dispatcher audit-safety tests

Both NotifyDispatcher_AuditWriter_Throws_DeliveryStillSucceeds and
NotificationDispatch_BrokenAuditWriter_StillTransitionsToDelivered read the
throwing writer's attempt counter with a bare Assert immediately after an
AwaitAssert on the Notifications row reaching Delivered. That assumes the audit
writes happen no later than the operational status write, which the dispatcher
deliberately does NOT guarantee: DeliverOneAsync persists the delivery state
first (NotificationOutboxActor.cs:657) and only then emits the Attempted
(:663) and terminal (:676) audit rows — audit is best-effort and must never
gate the user-facing action. Observing Delivered therefore establishes no
happens-before edge with the writer, and under a loaded full-solution parallel
run the continuation after the DB write can be scheduled after the poll that
saw Delivered, so the counter reads 0 and the test fails with "saw 0".

Reproduced deterministically by delaying only the post-update audit emission,
which yields both observed failure messages verbatim; with the fix in place the
same injected delay passes, and suppressing the emissions entirely still fails
both tests with the identical messages — the claims (delivery despite audit
failure, and attempts >= N) are unchanged in force, only the ordering
assumption is gone.

Test-only change; the update-then-audit ordering predates the remediation
(#23 M4) and is correct as written.
This commit is contained in:
Joseph Doherty
2026-08-14 23:12:06 -04:00
parent b1de9dfdd4
commit c4caebe9b4
2 changed files with 34 additions and 4 deletions
@@ -330,8 +330,22 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixture<MsSqlMigratio
Assert.NotNull(row.DeliveredAt);
}, TimeSpan.FromSeconds(15));
Assert.True(throwingWriter.Attempts >= 1,
$"Expected dispatcher to attempt audit write at least once; saw {throwingWriter.Attempts}.");
// AwaitAssert, not a bare Assert: the dispatcher persists the delivery
// state BEFORE emitting the audit rows (DeliverOneAsync updates the
// row, then emits Attempted, then the terminal), so seeing Delivered
// above orders nothing with respect to the audit write — on a loaded
// parallel run the post-write continuation can land after the poll that
// observed Delivered, producing a spurious "saw 0". The bounded wait
// keeps the assertion's force: the writer must actually be invoked
// within the timeout or the test fails exactly as before.
await AwaitAssertAsync(
() =>
{
Assert.True(throwingWriter.Attempts >= 1,
$"Expected dispatcher to attempt audit write at least once; saw {throwingWriter.Attempts}.");
return Task.CompletedTask;
},
TimeSpan.FromSeconds(15));
}
// ---------------------------------------------------------------------