Skip to content

Commit 7597547

Browse files
Expire a claim by the test's clock, not the database's (#166)
AnExpiredTokenDoesNotCompleteAClaim has been failing on main since some time on 2026-08-31, with nothing changed to cause it. The date caused it. ExpireAsync wrote `expires_at = now() - interval '1 day'` -- the database's wall clock -- while the query it is setting up compares expires_at against the injected clock. The two agreed only while real time stayed behind the seeded clock plus the lifetime under test, and that window closed on its own: Seed.Now 2026-07-30 12:00Z + PendingLifetime + 1 day 2026-08-30 12:00Z <- what the query compares now() - 1 day 2026-08-30 15:25Z <- what the row said Once the real calendar passed 2026-08-31 12:00Z the row stopped being in the past as far as the query was concerned, so an expired token began reading as Verified. The last green run on main was 2026-08-29. The clock is injected precisely so that "later" is a fact rather than a sleep; reaching for now() in the middle of that gave the test a dependency on the day it happened to run. It now expires relative to the clock the query uses, which is deterministic by construction. Found while opening #165, whose CI this was failing. Unrelated to that change -- it reproduces on unmodified main. 641 MUI.Catalog.Tests green. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent f04b833 commit 7597547

1 file changed

Lines changed: 20 additions & 4 deletions

File tree

tests/MUI.Catalog.Tests/Persistence/Claims/ClaimPostgresTests.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public async Task AnExpiredTokenDoesNotCompleteAClaim()
146146
var claim = await service.IssueAsync(game, user);
147147

148148
clock.Advance(ClaimToken.PendingLifetime + TimeSpan.FromDays(1));
149-
await ExpireAsync(db, claim.Id);
149+
await ExpireAsync(db, claim.Id, clock.GetUtcNow());
150150

151151
var verdict = await service.OfferBeaconAsync(game, claim.Token, ClaimChannel.Mssp);
152152

@@ -442,13 +442,29 @@ private static async Task<bool> IsClaimedAsync(TestDatabase db, Guid game)
442442
"SELECT is_claimed FROM game WHERE id = @game", new { game });
443443
}
444444

445-
private static async Task ExpireAsync(TestDatabase db, Guid claim)
445+
/// <summary>
446+
/// Expires a claim as of the test's own clock rather than the database's.
447+
/// </summary>
448+
/// <remarks>
449+
/// This used to write <c>now() - interval '1 day'</c>, which is the database's wall clock, while
450+
/// the query it is setting up compares <c>expires_at</c> against the injected clock. The two
451+
/// agreed only for as long as real time stayed behind the seeded clock plus the lifetime under
452+
/// test — a window that closed on its own. <see cref="Seed.Now"/> is 2026-07-30 12:00Z, the test
453+
/// advances 31 days to 2026-08-30 12:00Z, and once the real calendar reached 2026-08-31 12:00Z
454+
/// the row written here stopped being in the past as far as the query was concerned, so an
455+
/// expired token started reading as <c>Verified</c>. Nothing changed in the code; the date did.
456+
/// <para>
457+
/// The clock is passed in for the same reason it exists: so "later" is a fact rather than a
458+
/// sleep, and so the test says the same thing on every day it is run.
459+
/// </para>
460+
/// </remarks>
461+
private static async Task ExpireAsync(TestDatabase db, Guid claim, DateTimeOffset asOf)
446462
{
447463
await using var connection = await db.DataSource.OpenConnectionAsync();
448464

449465
await connection.ExecuteAsync(
450-
"UPDATE game_claim SET expires_at = now() - interval '1 day' WHERE id = @claim",
451-
new { claim });
466+
"UPDATE game_claim SET expires_at = @expiresAt WHERE id = @claim",
467+
new { claim, expiresAt = asOf - TimeSpan.FromDays(1) });
452468
}
453469

454470
/// <summary>A clock a test can push forward, so "later" is a fact rather than a sleep.</summary>

0 commit comments

Comments
 (0)