test(admin): extract AdminGate, pin safety properties via regression test#56
Merged
TortoiseWolfe merged 1 commit intomainfrom Apr 26, 2026
Merged
Conversation
…test The auth-race regressions in commits 6b4c13a, 2c97e67, 259b38d kept landing because the safety properties they violated had no test coverage. The same shape exists in AdminGate (wasAdmin.current debounce + cancelled flag + [user, authLoading, router] dep array) — defended in code, but a future refactor could silently strip any of them and the bug would return without CI catching it. This commit moves AdminGate out of admin/layout.tsx into its own file so it can be unit-tested in isolation, then adds 10 regression cases that pin each safety property as a contract: - Loading spinner before authLoading resolves - Loading spinner while admin check is in flight - Children render on admin=true - router.push('/') when admin=false on first mount (and never-was) - wasAdmin.current debounce: no redirect on transient admin→non-admin flip after the user was already admin - Dep-array integrity: user.id change re-runs checkIsAdmin - cancelled flag: no setState after unmount mid-flight - Renders nothing (not the nav) when admin=false and never-was - No admin check started while authLoading is true - No admin check started when user is null Refs #51. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 27, 2026
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>
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
Refs #51. The auth-race regression that's been reverted three times in
ProtectedRoute(commits 6b4c13a, 2c97e67, 259b38d) survives via a defensive shape: awasX.currentref that debounces transient flips, acancelledflag in async effects, and a[user, ...]dep array that re-runs the check on identity change.AdminGate(the admin RPC check that layers insideProtectedRoute) implements the same defensive shape, but with zero test coverage. A future refactor could silently strip any of these defenses and CI would say "all green" — exactly how the prior regressions kept landing in the first place.This PR pins those defenses as contracts. No behavior change; the bug shape that #51's body described as "latent stale closure" was actually already defended in code. The real gap was test coverage for that defense.
What changed
Extract
AdminGatefromsrc/app/admin/layout.tsxintosrc/app/admin/AdminGate.tsxso it's unit-testable in isolation.layout.tsxnow just composes<ProtectedRoute><AdminGate>{children}</AdminGate></ProtectedRoute>.Document the safety properties as a top-of-file comment naming each load-bearing ref/flag/dep so the next reader understands what must not be removed.
Add
src/app/admin/AdminGate.test.tsxwith 10 regression cases pinning each property:admin=truerouter.push('/')whenadmin=falseon first mount (and never-was)wasAdmin.currentdebounce: no redirect on transient admin→non-admin flipuser.idchange re-runscheckIsAdmincancelledflag: no setState after unmount mid-flightadmin=falseand never-wasauthLoadingis trueuseris nullOut of scope (intentionally)
#51's original plan listed three more steps: pin
user.idvia a paralleluserRef, hoist into a shareduseAuthGatehook, and sweep the codebase for the same shape. After reading the code carefully, the first looks like premature optimization (the existingcancelledflag + dep array catches the failure mode the userRef would defend against). The second is a real DRY opportunity but warrants its own focused refactor PR. The third is a sweep that should run on a clean baseline — i.e., after this PR lands.If the regression cases above ever fail in CI, that's the empirical justification for steps 2–4. Until then: YAGNI.
Test plan
pnpm vitest run src/app/admin/AdminGate.test.tsx— 10/10 pass in 62mspnpm vitest run src/app/admin src/components/auth tests/unit/services/admin tests/unit/auth— 36 files, 209 tests, all pass (including ProtectedRoute and AdminAuthService dependents)pnpm run type-check— cleanpnpm exec eslint src/app/admin/*— clean🤖 Generated with Claude Code