Stabilize the live-Postgres CI gate (#118 follow-up) - #120
Merged
Conversation
Three real bugs in the live-suite gate, each found by actually running it repeatedly, not by reading the code: 1. rbac_layer2_live_postgres.rs (added in #112, the day after #102's own fix landed in ca653a1) never picked up ca653a1's per-binary TEST_MUTEX pattern. Its 7 tests all call setup(), which calls rotate_signing_key (an insert) plus several distinct seeding query shapes, racing on Postgres's own pg_type catalog exactly the way ca653a1 already diagnosed and fixed for six other files. Confirmed structurally via git history (this file postdates the fix) and by grep (every other multi-test live suite has TEST_MUTEX; this one didn't). Fixed by applying the identical, already-proven pattern. crates/sms-worker/tests/live_postgres.rs had the same gap from the other direction: it predates ca653a1 by months (#92) and was never updated, protected only by being #[ignore]d and never run in CI until #118. Its four tests use distinct RoleLease role keys so they don't race on lock *state*, but that doesn't protect against the unrelated pg_type first-use race, which is about query-shape preparation, not which lock is held. Same fix applied. Reproduction: could not reproduce the pg_type collision locally despite 9 attempts across escalating conditions (default, 32x oversubscribed test threads, container CPU throttled to 0.5 and 0.25 cores, full-workspace cold sweeps) — consistent with this being a genuine low-probability race that needs CI's slower/fewer- core runners to reliably manifest, as ca653a1 itself found. The fix is the existing, already-proven mitigation applied to the two files that were missing it, not a new mechanism. 2. sms-test-support's stale-schema detection keyed on whether public.messages merely exists in the template database, not on whether it reflects the current migration files. Because the container (and its template) is deliberately left running between local runs, a bootstrap-only schema edit (0002_bootstrap changed, 0001_init untouched) was silently invisible to every suite until something forced a full `just test-live-clean` — already bit one real PR, which only passed after a forced clean rebuild. Fixed with a content fingerprint (FNV-1a over every up.sql's path and bytes) stamped onto the template via `COMMENT ON DATABASE` (pg_shdescription — cluster metadata, never copied into per-binary scratch databases, cleaned up automatically when the template is dropped). Any mismatch triggers a full drop-and-remigrate. Verified empirically: built a baseline 14-row operator_prefix_rules template with the old code, edited 0002_bootstrap to add a 15th row without touching 0001_init, reran with the old code (bug reproduced: count silently stayed at 14, tests still reported ok), then reran the identical scenario with the fix (count correctly became 15, fingerprint changed, no manual clean needed). 3. Folded in a test-only fix to claim_live_postgres.rs's two_concurrent_claimers_never_both_win_the_same_row, per a parallel investigation's finding: it asserted the seeded row appears in at most one claimer's combined output, which wrongly flags the harmless accepted->queued routing hop (deliberately lease-free, per Claimable for Message's own take_lease doc) as a false double-claim. Now asserts the real invariant — at most one claimer may reach `routed`, the only state that triggers a submission. Sanity-checked by temporarily removing if_match from the queued->routed write locally (never committed) and confirming the suite still compiles and runs against the real path cleanly; full CAS soundness was independently verified by the parallel investigation (240 concurrent attempts, exactly one winner per round). Verification: just check/lint/parity/assert-no-raw-sqlx all green. Three consecutive full `cargo test --workspace -- --ignored` sweeps (two against a warm container, one against a freshly cleaned cold container) all green, 37/37 test groups, 0 failures. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
4 tasks
stephane-segning
added a commit
that referenced
this pull request
Aug 7, 2026
…NTS.md (#123) Four PRs landed in one session (#118 harness, #120 gate stabilisation, #119 ProviderError::Indeterminate, #121 fake Orange + chaos suite) and the context file only picked up what the last two agents wrote inline. The biggest omission was the most consequential change: `sms-test-support` and the fact that CI now runs the 14 live-Postgres suites at all — previously `cargo test --workspace` ran without `--ignored`, so they were skipped in CI and had only ever run by hand. Records, in each case why it exists rather than just that it does: - The harness's five load-bearing constraints, each learned by something breaking: no container handle in a `static` (Drop never runs for statics, which leaked 56 containers), label-scoped cleanup only (an image-scoped sweep would destroy a developer's own database), one database per test binary (a shared one let a real dispatch subprocess steal other suites' messages), migration-content fingerprinting (an existence check served a stale schema silently), and the global container name that makes concurrent test runs corrupt each other. - That `ca653a1`'s mutex was never applied to two suites, both of which flaked in CI within hours of #118 running them for the first time. - `Indeterminate`'s connect-vs-read predicate, the `routed -> uncertain` edge it needed, and the fact that it trades a possibly lost message for never sending a duplicate — a product decision, not a free win. - A CLI-skew warning placed at both sites that tell you to run `cratestack migrate diff`, since the installed 0.7.4 CLI emits DDL the pinned 0.6.7 library never does. The warning previously existed only in an unrelated paragraph 150 lines away from the commands themselves. - The missing-`hasRole('system')` gap as an explicitly *unguarded* failure mode, now found seven times. The three entries above it each turned a silent failure loud; this one has no equivalent, which is why it recurs. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
crates/sms-auth/tests/rbac_layer2_live_postgres.rs— addsca653a1's per-binaryTEST_MUTEXpattern, missing since this file's own PR (RBAC layer 2: permission/scope enforcement (#24) #112) landed the day afterca653a1established it.crates/sms-worker/tests/live_postgres.rs— same pattern added; this file predatesca653a1by months and was never updated, protected only by being#[ignore]d and never run in CI until Add a testcontainers-backed live-Postgres harness; wire it into CI (#102) #118.crates/sms-test-support/src/lib.rs— stale-schema detection now keys on a content fingerprint ofschema/migrations/postgres/**, not on whetherpublic.messagesmerely exists.crates/sms-worker/tests/claim_live_postgres.rs— test-only fix totwo_concurrent_claimers_never_both_win_the_same_row's assertion (folded in from a parallel investigation into the same CI job — see Scope).Intent
Source of truth: the three-way CI failure this branch was created to diagnose — #118's new
live-Postgres suites (sms-test-support)job failing on a different test almost every run, three runs producing three different failures in three different test binaries. This PR addresses the one assigned to this investigation directly (sms-auth::rbac_layer2_live_postgres::provider_write_route_denies_a_token_that_is_correctly_scoped_for_something_else, failing withduplicate key value violates unique constraint "pg_type_typname_nsp_index"insiderotate_signing_key), plus a second, previously-undetected gap in the same class found while auditing every live suite for it, plus the harness's own stale-schema-detection bug named explicitly in the investigation brief.Scope
pg_typecollision's root cause and fix, an audit of every one of the 14 live suites for the same missing-mutex shape, the harness's stale-schema-detection bug, and (per an explicit scope hand-off partway through this investigation, once a parallel investigation into a different CI failure on this same job concluded the claim-loop code itself was sound) a test-only assertion fix inclaim_live_postgres.rs.crates/sms-worker/src/claim.rswas read and, briefly, locally and temporarily edited to sanity-check the new assertion's sensitivity to a real regression, then fully reverted (git diffon that file is empty in this PR). Also out of scope: the fixed test-harness container name's cross-process-interference footgun, found live during this investigation when a concurrently-running parallel investigation's own test runs collided with this one on the same container — flagged as a follow-up task rather than folded into this PR, since it's a local-multi-session concern, not a CI-gate reliability one (CI never runs two concurrentcargo test --workspace -- --ignoredinvocations against the same container).Verification
The
pg_typeroot cause, and how it was proven:ca653a1(#102) established a per-binarytokio::sync::Mutexevery test in a live suite acquires for its whole body, to serialize a binary's own concurrently-running tests against Postgres's own first-usepg_typecatalog race. Two files never got this pattern:rbac_layer2_live_postgres.rs(added in #112, dated 2026-08-03 11:05 — ten hours afterca653a1landed at 01:19 the same day, so the pattern already existed and simply wasn't applied) andsms-worker/tests/live_postgres.rs(added in #92, months beforeca653a1existed, and never revisited since — protected only by being#[ignore]d, and #118 is the first time anything ran it in CI). Confirmed viagit log/grepacross every*_live*.rsfile in the workspace: every other multi-test live suite hasTEST_MUTEX; these two didn't. Fixed by applying the identical, already-proven mechanism — no new mitigation invented.Reproduction attempts (honest reporting, not a clean repro): could not reproduce the
pg_typecollision locally despite 9 escalating attempts — default settings, 32x oversubscribed--test-threads, the harness container throttled to--cpus 0.5and--cpus 0.25, and full-workspace cold sweeps at each throttle level. This is consistent with the investigation brief's own framing ("It reproduces in CI, not on this fast machine") and withca653a1's own finding that this race is real but low-probability, needing conditions this fast, many-core dev machine doesn't reliably hit. The fix's correctness rests on it being the exact, already-validatedca653a1mechanism applied to files that were missing it — not on a fresh local reproduction.The stale-schema bug, proven end to end: built a baseline template with the old harness code (
operator_prefix_rules: 14 rows, matchingAGENTS.md's documented seed count). Edited0002_bootstrap/up.sqlto add a 15th row (a throwaway probe, reverted before committing —git diffon that file is empty in this PR) without touching0001_init. Reran a live suite with the old code: bug reproduced — row count silently stayed at 14,cargo teststill reportedok, exactly the "harness runs tests against a schema older than the working tree" defect described in the brief. Reran the identical scenario with the fix: row count correctly became 15, theCOMMENT ON DATABASEfingerprint changed, no manualjust test-live-cleanneeded — the harness self-healed on its very next call.claim_live_postgres.rs: rebuilt and ran the full 7-test suite 3 consecutive times with the real (unmodified)claim.rs— all green. Separately, temporarily removed.if_match(self.version)from the onequeued -> routedwrite path, ran the target test 3 times (all still passed — a genuinely narrow race window on this machine, consistent with thepg_typenon-repro pattern above), then fully reverted the change (confirmed viagit diffshowing zero delta) before proceeding. CAS soundness itself (240 concurrent attempts, exactly one winner per round, byte-identicalif_matchbetween the pinned0.6.7and upstream0.7.5) was independently established by the parallel investigation this fix was handed off from.Full-suite stability:
just check,just lint(fmt + clippy-D warnings),just parity,./ci/assert-no-raw-sqlx.sh,just testall green. Three consecutive fullcargo test --workspace -- --ignoredsweeps — two against a warm/reused container, one against a freshlytest-live-clean'd cold container — all green, 37/37 test-result groups, 0 failures, 0 flakes.Container safety: every container operation in this investigation was scoped to the
dev.vsms.test-harness=truelabel (verified viadocker inspectbefore touching it), never by image or bare name. Finaldocker ps -acontainer count and names match this machine's pre-investigation baseline exactly (the 11 pre-existingtenant-provisioner-*containers, untouched, plus the onevsms-test-harness-postgres).Screenshots/Evidence
N/A — backend test-infrastructure fixes, no UI surface. Log excerpts and exact commands are in this PR's commit message and available on request; the key numbers are summarized in Verification above.
Risk Assessment
Low. No production code path changes (confirmed via
git diff --statagainstmaintouching onlycrates/sms-auth/tests/,crates/sms-worker/tests/, andcrates/sms-test-support/src/lib.rs— the latter is test-infrastructure-only, allowlisted under R1 as the "migrations" exception). Thepg_typefix is the lowest-risk kind of change possible: applying an already-shipped, already-proven mitigation to files that were missing it, not inventing a new one. The stale-schema fix changes when the harness rebuilds its template (more often, on genuine content drift, never on a no-op rerun) — proven both ways (stale-repros, fix-heals) against a real container, not just reasoned about.AI Usage Declaration
This PR was implemented by Claude Code (Claude Sonnet 5) under my direction: diagnosing the
pg_typecollision's root cause via git history and cross-file auditing, fixing the harness's stale-schema-detection bug, and folding in a hand-off fix to a parallel investigation's test assertion.ca653a1, confirmed viagit log/grep, not assumed) and the fingerprint mechanism's design (whyCOMMENT ON DATABASE/pg_shdescriptionrather than an in-schema marker table — it must never be copied into per-binary scratch databases viaCREATE DATABASE ... TEMPLATE).cargo test(or a plausible-sounding fix) is not to be trusted without live verification.pg_typerace locally, I reported that honestly rather than claiming false confidence.Reviewer Focus
COMMENT ON DATABASE/pg_shdescriptionis the right place to stamp the migration fingerprint, versus (for example) a dedicated tracking table in the adminpostgresdatabase — the tradeoff considered was: a table would need explicit cleanup reasoning of its own, whereaspg_shdescriptionis cluster metadata that Postgres's owndropdb()cleans up for free and is structurally guaranteed to never leak into a per-binary scratch database.*_live*.rs/*_live_postgres.rsfile in the workspace (14 total, matching the harness's own documented count) for the shape "2+#[tokio::test]s, noTEST_MUTEX" and found exactly these two; worth a second pass in case the audit missed a naming variant.cargo test -- --ignoredinvocations — confirmed live during this investigation, when a parallel investigation's own runs collided with this one. Worth deciding whether it belongs in this PR's scope after all, or stays a follow-up.