Skip to content

[Gap-Audit] RLS test suite wedges on cloud Supabase when prior runs leave residue #50

@TortoiseWolfe

Description

@TortoiseWolfe

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-209createTestUser() 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.tsfileParallelism: false prevents same-run races; doesn't help with cross-run residue.

Gap

Three failure modes left unhandled:

  1. 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.
  2. 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.
  3. 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:

  1. 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
  2. Wire it via globalSetup in vitest.rls.config.ts so it runs once before any test file
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggap-auditIdentified during 2026-04-25 planned-vs-shipped auditpriority:p2Medium — schedule (feature gaps, partial implementations)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions