Skip to content

feat(#454, #430): an is_admin E2E fixture, and the two AAA violations it found - #509

Merged
TortoiseWolfe merged 3 commits into
mainfrom
feat/admin-e2e-fixture
Aug 3, 2026
Merged

feat(#454, #430): an is_admin E2E fixture, and the two AAA violations it found#509
TortoiseWolfe merged 3 commits into
mainfrom
feat/admin-e2e-fixture

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #454. Closes #430. Supersedes draft PR #503.

No E2E run had ever rendered an admin page. AdminGate.tsx:81 bounces an authenticated non-admin to /, so three separate things were measuring the wrong page or nothing at all:

The fixture

seedIsolatedAdmin() promotes a throwaway user through user_profiles.is_admin — the single authority since #240, read live by the is_admin() SECURITY DEFINER RPC. One service-role UPDATE: no token refresh, no auth-hook registration, because no live RLS policy reads the JWT claim (every app_metadata mention in the migration is a comment or inside custom_access_token_hook).

It verifies itself through the user's own session and throws otherwise. A silently-unpromoted user still renders — it renders the redirect — so every assertion downstream would measure the home page and pass. That is #454's defect exactly, and without the check the fixture built to fix it would have reproduced it. The service-role client cannot answer the question; it bypasses RLS and would say "true" regardless.

Not the shared storage-state user, deliberately. is_admin() gates 25 policies across 9 tables — payments, messages, conversations, profiles, connections, subscriptions, audit logs, rate limits. Promoting the shared user is one line and would have given every existing spec cross-user reads, with row-count assertions still passing while measuring something else.

What unblinding found — two real AAA violations

Both on /admin/email, both invisible until now:

.stat-title           #484f58 on #ebe5dd   6.62:1   ×3   ("Remaining", "Status", "Window resets")
text-base-content/70  #5f656d on #f5f0eb   5.19:1   ×2   ("Priority 1", "Priority 2")

The first is the one worth reading. globals.css had already corrected .stat-title, raising alpha to 80% and documenting "~7.1:1 on light". Measured: 6.62. That 7.1 was computed against base-100; a .stats block sits on base-200 — precisely the trap #462 records, where /80 measures 7.08 on base-100 and 6.4–6.5 on base-200. A correction that was right for the surface it was checked on and wrong for the one it renders on, undetected because nothing could reach the page. Now solid, like #411, #425 and #495 before it.

Coverage moved

gate before after
landmark sweep 36 paths, 9 exclusions 42 paths, 3 exclusions
contrast sweep 36 paths × 2 themes 42 × 2 — 85 passing

Verification

Mutation-tested against the original defect. Revert the sh-well wrapper and admin-depth reports 0/1 scrollers welled and fails by name — not a vacuous pass, it measured one scroller and found zero welled.

The spec asserts the invariant (every scroller is wrapped) rather than the element, because /admin/messaging's well is behind top_senders.length > 0 — asserting it unconditionally would be a data-dependent element, which is how #495 looked like a flake. A minScrollers floor stops the invariant passing vacuously, and welled/total prints every run so "0/0" can never be mistaken for coverage.

All numbers above measured against a real root build (DISABLE_BASE_PATH=true, serve out), as CI runs these specs.

Three of my own errors, caught by running rather than reasoning

  1. Deleting the landmarks exclusion alone would have re-created The AAA contrast sweep reports six /admin routes as swept while measuring the home page — the gap is admin PRIVILEGE, not auth #454's defect there — those six routes redirect to /, which has a <main> and a skip link, so they would have passed while measuring the home page.
  2. The first admin navigation double-navigated, hitting the basePath trap (an absolute path replaces the basePath rather than appending) and 404'ing all six routes.
  3. The first two .stat-title fixes lost the cascade to a later theme-scoped rule — which is how the existing block with the wrong arithmetic turned up.

Also

auth.setup.ts gains an orphan sweep: an is_admin throwaway can read every message and payment in the shared project, so a failed teardown must not be silent. It pages through listUsers#197 truncates at 50, and a sweep that reads page one and reports "0 orphans" is the same shape as everything else this repo keeps finding.

Draft PR #503 can be closed — its commit is cherry-picked here and now has the verification it never had.

🤖 Generated with Claude Code

TurtleWolfe and others added 3 commits August 3, 2026 16:59
…RIFIED

The deferred half of #430. Both scrollers were a bare
`<div className="overflow-x-auto">` at AdminPaymentPanel.tsx:216 and
AdminMessagingOverview.tsx:173 — the line numbers in the ticket were accurate,
unlike #373's, which drifted four times.

`sh-well` paints an inset shadow BELOW its children, so it needs a padded
parent: putting it on the `overflow-x-auto` div itself would clip the shadow
inside the scroller and hide it under the table. So the scroller is WRAPPED by
`sh-well rounded-lg p-2` rather than annotated.

WHAT IS NOT VERIFIED, AND WHY THIS IS NOT PUSHED.

I could not confirm the wells actually PAINT. Both routes are auth-gated and the
E2E storage state at tests/e2e/fixtures/storage-state-auth.json is stale — with
it loaded, /admin/payments redirects to the home page, so the components never
render and a `.sh-well` query returns 0.

That matters more than usual here: these two items were DEFERRED from #430
precisely because a sh-well with the wrong parent is a visual no-op, and "a class
being present proves nothing about whether it renders" has been true four times
in this session. Type-check clean and 35 component tests passing do not answer
the question that was actually asked.

Hit my own documented trap on the way: a `{/* comment */}` placed between
`{cond ? (` and its element is two root nodes and a syntax error. The comment now
sits above the ternary.

Refs #430 — needs an authenticated visual check before this ships.
… it found

No E2E run had ever rendered an admin page. `AdminGate.tsx:81` bounces an
authenticated non-admin to `/`, so three separate things were measuring the
wrong page or nothing: the AAA sweep reported six `/admin*` routes as swept
while measuring the HOME page, the admin table wells could not be verified
(draft PR #503 sat 17/17 green and unmergeable), and the landmark gate had to
exclude those six outright.

`seedIsolatedAdmin()` promotes a throwaway user through
`user_profiles.is_admin` — the single authority since #240, read live by the
`is_admin()` SECURITY DEFINER RPC. One service-role UPDATE; no token refresh
and no auth-hook registration, because no live RLS policy reads the JWT claim.

It VERIFIES ITSELF through the user's own session and throws otherwise. A
silently-unpromoted user still renders — it renders the redirect — so every
assertion downstream would measure the home page and pass. That is the defect
#454 is about, and without the check the fixture built to fix it would
reproduce it.

Not the shared storage-state user, deliberately: `is_admin()` gates 25 policies
across 9 tables, so promoting it would give every existing spec cross-user
reads and row-count assertions would keep passing while measuring something
else.

Unblinding the sweep immediately found two real AAA violations on
/admin/email, both invisible until now:

  .stat-title           #484f58 on #ebe5dd  6.62:1   (3 elements)
  text-base-content/70  #5f656d on #f5f0eb  5.19:1   (2 elements)

The first is the sharper one. globals.css ALREADY corrected `.stat-title`,
raising alpha to 80% and documenting "~7.1:1 on light". Measured: 6.62. 7.1 was
computed against base-100; a `.stats` block sits on base-200 — exactly the trap
#462 records, where /80 measures 7.08 on base-100 and 6.4-6.5 on base-200. The
alpha was right for the surface it was checked on and wrong for the one it
renders on. Now solid, like #411, #425 and #495 before it.

Coverage: landmark sweep 36 -> 42 paths (exclusions 9 -> 3); contrast sweep
36 -> 42 paths x 2 themes, 85 passing.

Mutation-tested against the original defect: with the sh-well wrapper reverted,
admin-depth reports `0/1 scrollers welled` and fails by name. The spec asserts
the INVARIANT (every scroller is welled) rather than the element, because
/admin/messaging's well is behind `top_senders.length > 0` — a data-dependent
element is how #495 looked like a flake. A floor stops it passing vacuously.

Orphan sweep in auth.setup.ts pages through listUsers (#197 truncates at 50)
and deletes throwaway admins that survived a previous run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d not see it

CI found both defects on the first run that could reach an admin page as an
admin — which is the entire argument for #454's fixture.

1. #430's fix wrapped the two HAND-ROLLED scrollers in AdminPaymentPanel and
   AdminMessagingOverview and missed `AdminDataTable`, the shared component
   several admin surfaces render. It only appears when there is data, so a
   local project with no conversations never rendered it. CI's shared project
   has data, and the spec reported it by name:

     /admin/messaging: 1 scroller(s) are not wrapped in .sh-well
     unwelled parent: "flex flex-col gap-3 mt-8"

   Wrapping it in AdminDataTable fixes every consumer at once.

2. The spec itself was timing-dependent, and that is why CI called it FLAKY
   rather than failed. `page.evaluate` does not retry the way a web-first
   assertion does (#396, instance 5) — it snapshots the DOM once. The admin
   panels render a `loading-spinner` until their RPCs return, so counting too
   early sees zero scrollers and passes on nothing. Measured: a 3.5s settle
   read 0/0 on /admin/payments, which had read 1/1 moments earlier.

   It now waits for `.loading-spinner` to reach count 0 — a signal, not a
   sleep — and re-reads through `expect.poll`. /admin/messaging goes from 0/0
   to 1/1 locally as a result: the scroller was always there, the spec just
   measured before it existed.

The flake was the tell. A test that passes when the data is absent and fails
when it is present is not flaky; it is a defect plus a vacuous pass, which is
the shape #495 wore.

No mutation test needed for the AdminDataTable fix: CI already ran it against
the original defect and failed by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TortoiseWolfe
TortoiseWolfe merged commit eda1a80 into main Aug 3, 2026
19 checks passed
@TortoiseWolfe
TortoiseWolfe deleted the feat/admin-e2e-fixture branch August 3, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants