Summary
pnpm test:rls against cloud Supabase fails on beforeAll if a prior run died mid-test and left orphan rows that block deletion of the canonical *@scripthammer.test users. The test fixture's afterAll cleanup is correct for successful runs; what's missing is recovery from killed runs.
Surfaced while verifying #44 — cloud had two stale *@scripthammer.test users from a 2026-04-16 run with one orphaned payment_intents row referencing userA. The user delete failed with payment_intents_template_user_id_fkey (FK constraint), wedging every test file that called createTestUser('test-user-a@scripthammer.test', …) in its beforeAll.
What's shipped
tests/fixtures/test-users.ts:155-209 — createTestUser() has a delete-and-recreate retry path for "already registered" errors. The retry uses auth.admin.deleteUser(), which fails when payment FKs reference the user.
tests/rls/payment-rls.test.ts:67-73 — per-describe-block afterAll cleans up payment intents/results then deletes test users. Works perfectly when tests complete.
vitest.rls.config.ts — fileParallelism: false prevents same-run races; doesn't help with cross-run residue.
Gap
Three failure modes left unhandled:
- Killed run during a test body — a
kill -9 on the runner skips afterAll. Next run inherits orphan payment_intents, payment_results, subscriptions, webhook_events rows.
- Soft-deleted users with reserved emails — Supabase's auth uniqueness can hold the email after a soft delete, blocking recreation. The fixture currently calls
deleteUser(id) without should_soft_delete=false.
- Cloud-only residue — local
docker compose --profile supabase down -v wipes everything; cloud Supabase persists indefinitely until something cleans it.
Plan
Idempotent precondition cleanup at fixture startup:
- Add
tests/rls/__setup__/cleanup-stale.ts (or extend tests/fixtures/test-users.ts) — a globalSetup hook for vitest.rls.config.ts that:
- Lists all
*@scripthammer.test auth users
- For each, deletes their dependent rows via service role:
payment_intents, payment_results, subscriptions, then the user itself with should_soft_delete=false
- Wire it via
globalSetup in vitest.rls.config.ts so it runs once before any test file
- Document it in the fixture comment so future test authors know the cleanup is centralized
Reference recipe (this is what unblocked the cloud verification in #44):
// 1. orphan intents → 2. their results → 3. subscriptions → 4. user (hard delete)
const intents = await fetch('/rest/v1/payment_intents?template_user_id=eq.' + uid + '&select=id', {headers}).then(r=>r.json());
for (const i of intents) await fetch('/rest/v1/payment_results?intent_id=eq.' + i.id, {method:'DELETE',headers});
await fetch('/rest/v1/payment_intents?template_user_id=eq.' + uid, {method:'DELETE',headers});
await fetch('/rest/v1/subscriptions?template_user_id=eq.' + uid, {method:'DELETE',headers});
await fetch('/auth/v1/admin/users/' + uid + '?should_soft_delete=false', {method:'DELETE',headers});
Reference
Summary
pnpm test:rlsagainst cloud Supabase fails onbeforeAllif a prior run died mid-test and left orphan rows that block deletion of the canonical*@scripthammer.testusers. The test fixture'safterAllcleanup is correct for successful runs; what's missing is recovery from killed runs.Surfaced while verifying #44 — cloud had two stale
*@scripthammer.testusers from a 2026-04-16 run with one orphanedpayment_intentsrow referencinguserA. The user delete failed withpayment_intents_template_user_id_fkey(FK constraint), wedging every test file that calledcreateTestUser('test-user-a@scripthammer.test', …)in itsbeforeAll.What's shipped
tests/fixtures/test-users.ts:155-209—createTestUser()has a delete-and-recreate retry path for "already registered" errors. The retry usesauth.admin.deleteUser(), which fails when payment FKs reference the user.tests/rls/payment-rls.test.ts:67-73— per-describe-blockafterAllcleans up payment intents/results then deletes test users. Works perfectly when tests complete.vitest.rls.config.ts—fileParallelism: falseprevents same-run races; doesn't help with cross-run residue.Gap
Three failure modes left unhandled:
kill -9on the runner skipsafterAll. Next run inherits orphanpayment_intents,payment_results,subscriptions,webhook_eventsrows.deleteUser(id)withoutshould_soft_delete=false.docker compose --profile supabase down -vwipes everything; cloud Supabase persists indefinitely until something cleans it.Plan
Idempotent precondition cleanup at fixture startup:
tests/rls/__setup__/cleanup-stale.ts(or extendtests/fixtures/test-users.ts) — aglobalSetuphook forvitest.rls.config.tsthat:*@scripthammer.testauth userspayment_intents,payment_results,subscriptions, then the user itself withshould_soft_delete=falseglobalSetupinvitest.rls.config.tsso it runs once before any test fileReference recipe (this is what unblocked the cloud verification in #44):
Reference
044/payment-rls-verifytests/fixtures/test-users.tsvitest.rls.config.ts