Skip to content

fix(rls): cleanup-stale globalSetup so killed prior runs don't wedge pnpm test:rls (#50)#60

Merged
TortoiseWolfe merged 5 commits intomainfrom
050/rls-cleanup-stale
Apr 27, 2026
Merged

fix(rls): cleanup-stale globalSetup so killed prior runs don't wedge pnpm test:rls (#50)#60
TortoiseWolfe merged 5 commits intomainfrom
050/rls-cleanup-stale

Conversation

@TortoiseWolfe
Copy link
Copy Markdown
Owner

Closes #50.

Summary

Adds a Vitest globalSetup to pnpm test:rls that scrubs orphan *@scripthammer.test users and their FK-blocking dependent rows (payment_intents, subscriptions, user_profiles) before any RLS test file collects. The fixture's createTestUser retry path remains as the second line of defense; this hook just prevents the FK-block that defeated it on 2026-04-26 (and required manual psql cleanup that day).

Why this matters

pnpm test:rls against cloud Supabase fails on beforeAll if a prior run died mid-test and left orphan rows. The fixture's existing recovery path (tests/fixtures/test-users.ts:171-201) tries delete-then-recreate, but auth.admin.deleteUser hits payment_intents_template_user_id_fkey and gives up. The next operator has to manually grep for orphan FKs, delete dependent rows in the right order, then delete the user. We saw this concretely yesterday (2026-04-26) when residue from a 2026-04-16 run wedged the suite.

After this PR: the globalSetup walks the FK chain (payment_intents → subscriptions → user_profiles → auth.admin.deleteUser) for every *@scripthammer.test user, before tests collect. Killed runs become recoverable on the next invocation; the operator never has to know the chain.

What changed

  • tests/rls/__setup__/cleanup-stale-impl.ts (new) — pure async cleanupStaleScripthammerUsers(client, logger?)
  • tests/rls/__setup__/cleanup-stale.ts (new) — globalSetup wrapper; skips silently when env is absent
  • tests/unit/rls-cleanup.test.ts (new) — 4 unit cases pinning FK chain order, production-safety filter, best-effort error handling, no-op on empty
  • vitest.rls.config.ts — wire globalSetup: ['./tests/rls/__setup__/cleanup-stale.ts']
  • docs/superpowers/specs/2026-04-27-rls-cleanup-stale-design.md (new)
  • docs/superpowers/plans/2026-04-27-rls-cleanup-stale.md (new)

Empirical verification

  1. Inject a stale user via Supabase auth admin API: auth.admin.createUser({email: 'test-cleanup-probe@scripthammer.test', ...})
  2. pnpm test:rls
  3. Output includes: [rls cleanup-stale] removed 1 user(s); 0 error(s) logged
  4. Suite proceeds to 55/55 (after a brief auth admin rate-limit pause — known constraint of running cleanup + suite back-to-back on Supabase Cloud's free-tier quota)

Out of scope

Test plan

  • pnpm vitest run tests/unit/rls-cleanup.test.ts — 4/4 pass
  • pnpm test:rls against cloud, no stale users — still 55/55 (silent no-op cleanup)
  • pnpm test:rls against cloud, with injected stale user — cleanup logs the removal, suite proceeds to 55/55
  • pnpm run type-check — clean

🤖 Generated with Claude Code

TurtleWolfe and others added 5 commits April 27, 2026 16:02
Pure async function cleanupStaleScripthammerUsers walks the FK chain
(payment_intents → subscriptions → user_profiles → auth.deleteUser)
for every *@scripthammer.test user. Best-effort: errors are logged
and cleanup continues to the next step. Returns a summary count.

Tests pin the contract:
- Chain runs in correct order per user.
- Non-matching emails are never touched (production safety).
- Best-effort: a transient delete error doesn't halt the rest.
- No-op when no scripthammer.test users exist.

Refs #50.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Constructs a Supabase service client from env, calls
cleanupStaleScripthammerUsers, logs the summary. Skips silently when
SUPABASE_SERVICE_ROLE_KEY is missing — same gate the tests already
use via hasRlsTestEnvironment(). Wired into vitest.rls.config.ts in
the next commit.

Refs #50.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vitest.rls.config.ts now invokes cleanup-stale.ts as globalSetup, so
killed prior runs that left orphan FK rows referencing
*@scripthammer.test users get scrubbed before tests collect.

While verifying empirically against cloud, the original CleanupSummary
shape (per-table 'removed' counts) turned out to be misleading:
PostgREST DELETE succeeds regardless of row-match, so the summary
reported "1 intent removed" for a stale user that had no payment
intents. Replaced the per-table counts with two honest fields:

  - usersRemoved: count from auth.admin.deleteUser successes (the
    only DELETE whose success/failure carries real signal)
  - errorsLogged: count of per-table errors logged (non-fatal;
    cleanup continues)

Manual verification on cloud Supabase:
  - Inject a stale test-cleanup-probe@scripthammer.test user via
    auth admin API.
  - pnpm test:rls
  - Output includes: [rls cleanup-stale] removed 1 user(s); 0 error(s) logged
  - Suite proceeds to 55/55 (after a brief auth admin rate-limit
    pause — known constraint of running cleanup + suite back-to-back
    on Supabase Cloud's free-tier auth admin quota).

Refs #50.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the FK-chain order, scope-narrowing rationale (why this
doesn't unify with #57 even though I proposed that yesterday), and
the bite-sized task plan that produced the runtime commits. Future
operators can reconstruct the design intent without re-deriving from
the PR description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The implementation settled on { usersRemoved, errorsLogged } rather
than the per-table 'removed' fields the spec originally proposed,
because PostgREST DELETE returns success regardless of row-match
(would have given misleading counts). Updated the spec's verification
example + added a note explaining the change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TortoiseWolfe TortoiseWolfe merged commit ed21c21 into main Apr 27, 2026
28 checks passed
@TortoiseWolfe TortoiseWolfe deleted the 050/rls-cleanup-stale branch April 27, 2026 18:02
TortoiseWolfe added a commit that referenced this pull request Apr 27, 2026
…andoff (#61)

Captures end-of-session state after 6 PRs landed (#54, #55, #56, #58,
#59, #60). Family A is empty (both stability hotspots resolved).
Family D1 done. Recommended next pickup: B1 (#43 /payment/result page).
The handoff doc is the load-bearing artifact for the next operator —
it lists open issues by family, sharp edges, and a copy-pasteable
quick-start.

Co-authored-by: TurtleWolfe <TurtleWolfe@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants