test(e2e): shared session, db reset, and admin spec #377

Merged
james merged 1 commit from test/e2e-infra-hardening into main 2026-06-29 16:45:09 +00:00
Owner

Hardens the Playwright e2e harness (#326), building on the merged suite in apps/e2e/.

What's here

Gated, destructive reset endpointPOST /api/test/reset (apps/api/app/api/test/reset/route.ts)

  • Fail-closed: the gate is the FIRST thing the handler does — returns an inert 404 (via the existing notFound() helper) unless process.env.E2E_TEST_ROUTES === "1". No reveal, no per-user data out.
  • Optional zod body { includeIdentity?: boolean } (.strict()). Default = data-only: wipes every per-user domain table but preserves users / local_identities / oauth_identities / sessions so the shared session stays valid. includeIdentity: true = full wipe (used by the admin instance so the next registration is "first user = admin").
  • Per-table deleteFrom in child→parent order (derived from the Database interface) — not TRUNCATE (unsupported on SQLite), and not relying on FK cascade (documented as stale).
  • Allowlisted in EXACT_PUBLIC_ROUTES (the env gate is the real guard) and excluded from the OpenAPI contract via openapi-coverage.ts's exclusion set — openapi:check + openapi:coverage stay clean with no regenerated openapi.json.
  • Both-engine vitest (tests/api/test-reset.test.ts): gate cases (unset / non-"1" / prod-without-var → 404) plus describePerEngine proving data-only keeps the user+session and includeIdentity clears them.

⚠️ Reviewer attention — new public + destructive route. It deletes rows. It is dark on every real deployment because E2E_TEST_ROUTES is a test-only var (cf. TEST_POSTGRES_URL) that nothing but serve.sh sets. Deviation from the plan worth a look: the gate is E2E_TEST_ROUTES alone, not NODE_ENV === "production". The harness boots via next start, which forces NODE_ENV=production exactly like a real deploy, so a NODE_ENV check can't tell e2e from prod — it would make the route permanently dead (verified: a fresh next start with the var set still 404'd under the original gate). The explicit opt-in var is the only honest discriminator and is genuinely fail-closed.

storageState + reset model

  • tests/auth.setup.ts (the setup project) registers the shared user once on :3000 and saves storageState to apps/e2e/.auth/user.json.
  • fixtures/test.ts extends test with an auto fixture that POSTs the data-only reset before each test, so authed specs start logged-in with a clean slate and run order-free.
  • The 5 domain specs import test from fixtures/test, drop their per-spec registerFreshUser, and navigate straight in. The now-dead fixtures/auth.ts is removed; the profile-picture test.fixme stays.

Two-instance boot

  • serve.sh is env-driven (PORT / DB_FILE / pass-through REGISTRATION_POLICY, always export E2E_TEST_ROUTES=1).
  • playwright.config.ts boots a second admin-approval instance on :3100 (separate DB file) and partitions projects: setup, chromium (depends on setup + storageState, :3000), smoke (:3000, no storageState), admin (:3100).

Admin spectests/admin.spec.ts (serial, full reset in beforeEach): approval flow (first user = admin → second user in a second browser context registers into the pending screen and can't sign in → admin Approves in Account → second user signs in) + invite mint/reveal/Copy → revoke/confirm. New account.admin.* / register.* catalog values added to fixtures/strings.ts.

Heads-up: two stale assertions on main

The per-domain specs (#335) merged ~20 min after two client changes and were never re-run against them, so a couple of assertions were already broken on main. Fixed here as part of the hardening:

  • #373 (Chat is the default landing)smoke.spec.ts + the setup flow expected /notes; updated to /chat (Notes is reached by direct URL — it's no longer in the sidebar nav).
  • #371 (People/Org detail open in read mode)network.spec.ts filled the contact value / selected the key-person before revealing those forms; reordered to reveal-then-fill.

Verification (all green locally)

  • pnpm -F @carol/api test → 86 files, 1022 passed. Reset test green on SQLite + gate; Postgres leg runs in CI (TEST_POSTGRES_URL unset here).
  • openapi:check + openapi:coverage → clean.
  • semgrep --config p/nodejsscan --config .semgrep apps/api → 0 findings (277 files).
  • Built the bundle + API, booted both instances, ran Chromium: 11 passed, 1 skipped (setup → 5 domain specs + smoke on :3000, 2 admin on :3100; the skip is the profile-picture test.fixme).
  • tsc --noEmit (e2e) + lint (api) clean.

Closes #326
Refs #150

🤖 Generated with Claude Code

Hardens the Playwright e2e harness (#326), building on the merged suite in `apps/e2e/`. ## What's here **Gated, destructive reset endpoint** — `POST /api/test/reset` (`apps/api/app/api/test/reset/route.ts`) - **Fail-closed:** the gate is the FIRST thing the handler does — returns an inert `404` (via the existing `notFound()` helper) unless `process.env.E2E_TEST_ROUTES === "1"`. No reveal, no per-user data out. - Optional zod body `{ includeIdentity?: boolean }` (`.strict()`). Default = **data-only**: wipes every per-user domain table but preserves `users` / `local_identities` / `oauth_identities` / `sessions` so the shared session stays valid. `includeIdentity: true` = **full wipe** (used by the admin instance so the next registration is "first user = admin"). - Per-table `deleteFrom` in **child→parent order** (derived from the `Database` interface) — not `TRUNCATE` (unsupported on SQLite), and not relying on FK cascade (documented as stale). - Allowlisted in `EXACT_PUBLIC_ROUTES` (the env gate is the real guard) and excluded from the OpenAPI contract via `openapi-coverage.ts`'s exclusion set — `openapi:check` + `openapi:coverage` stay clean with no regenerated `openapi.json`. - Both-engine vitest (`tests/api/test-reset.test.ts`): gate cases (unset / non-"1" / prod-without-var → 404) plus `describePerEngine` proving data-only keeps the user+session and `includeIdentity` clears them. > ⚠️ **Reviewer attention — new public + destructive route.** It deletes rows. It is dark on every real deployment because `E2E_TEST_ROUTES` is a test-only var (cf. `TEST_POSTGRES_URL`) that nothing but `serve.sh` sets. **Deviation from the plan worth a look:** the gate is `E2E_TEST_ROUTES` **alone**, not `NODE_ENV === "production"`. The harness boots via `next start`, which forces `NODE_ENV=production` exactly like a real deploy, so a `NODE_ENV` check can't tell e2e from prod — it would make the route permanently dead (verified: a fresh `next start` with the var set still 404'd under the original gate). The explicit opt-in var is the only honest discriminator and is genuinely fail-closed. **storageState + reset model** - `tests/auth.setup.ts` (the `setup` project) registers the shared user once on `:3000` and saves `storageState` to `apps/e2e/.auth/user.json`. - `fixtures/test.ts` extends `test` with an auto fixture that POSTs the data-only reset before each test, so authed specs start logged-in with a clean slate and run order-free. - The 5 domain specs import `test` from `fixtures/test`, drop their per-spec `registerFreshUser`, and navigate straight in. The now-dead `fixtures/auth.ts` is removed; the profile-picture `test.fixme` stays. **Two-instance boot** - `serve.sh` is env-driven (`PORT` / `DB_FILE` / pass-through `REGISTRATION_POLICY`, always `export E2E_TEST_ROUTES=1`). - `playwright.config.ts` boots a second `admin-approval` instance on `:3100` (separate DB file) and partitions projects: `setup`, `chromium` (depends on setup + storageState, `:3000`), `smoke` (`:3000`, no storageState), `admin` (`:3100`). **Admin spec** — `tests/admin.spec.ts` (serial, full reset in `beforeEach`): approval flow (first user = admin → second user in a second browser context registers into the pending screen and can't sign in → admin Approves in Account → second user signs in) + invite mint/reveal/Copy → revoke/confirm. New `account.admin.*` / `register.*` catalog values added to `fixtures/strings.ts`. ## Heads-up: two stale assertions on `main` The per-domain specs (#335) merged ~20 min after two client changes and were never re-run against them, so a couple of assertions were already broken on `main`. Fixed here as part of the hardening: - **#373 (Chat is the default landing)** — `smoke.spec.ts` + the setup flow expected `/notes`; updated to `/chat` (Notes is reached by direct URL — it's no longer in the sidebar nav). - **#371 (People/Org detail open in read mode)** — `network.spec.ts` filled the contact value / selected the key-person before revealing those forms; reordered to reveal-then-fill. ## Verification (all green locally) - `pnpm -F @carol/api test` → 86 files, 1022 passed. Reset test green on SQLite + gate; **Postgres leg runs in CI** (`TEST_POSTGRES_URL` unset here). - `openapi:check` + `openapi:coverage` → clean. - `semgrep --config p/nodejsscan --config .semgrep apps/api` → 0 findings (277 files). - Built the bundle + API, booted both instances, ran Chromium: **11 passed, 1 skipped** (`setup` → 5 domain specs + smoke on `:3000`, 2 admin on `:3100`; the skip is the profile-picture `test.fixme`). - `tsc --noEmit` (e2e) + `lint` (api) clean. Closes #326 Refs #150 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(e2e): shared session, db reset, and admin spec
All checks were successful
PR / OpenAPI (pull_request) Successful in 1m43s
PR / Static analysis (pull_request) Successful in 1m45s
PR / OSV-Scanner (pull_request) Successful in 56s
PR / Build (pull_request) Successful in 2m48s
PR / pnpm audit (pull_request) Successful in 1m14s
PR / Test (postgres) (pull_request) Successful in 2m8s
PR / Package age policy (soft) (pull_request) Successful in 22s
Secrets / gitleaks (pull_request) Successful in 22s
PR / Test (sqlite) (pull_request) Successful in 2m18s
PR / E2E (Playwright) (pull_request) Successful in 4m24s
PR / Trivy (image) (pull_request) Successful in 1m54s
PR / Coverage (soft) (pull_request) Successful in 1m44s
PR / Client (web export smoke) (pull_request) Successful in 6m0s
PR / Lint (pull_request) Successful in 17m16s
PR / Typecheck (pull_request) Successful in 17m21s
Commits / Conventional Commits (pull_request) Successful in 38s
33b8014319
Harden the Playwright harness (#326): a gated test-only DB reset endpoint,
a shared logged-in storageState, two-instance boot, and the admin spec.

- Add POST /api/test/reset (apps/api): fail-closed behind E2E_TEST_ROUTES=1,
  per-table deleteFrom in child->parent order (portable to SQLite + Postgres),
  data-only by default or full wipe with { includeIdentity: true }. Excluded
  from the OpenAPI contract; both-engine vitest covers the gate + both depths.
- serve.sh is env-driven (PORT / DB_FILE / REGISTRATION_POLICY) and always
  exports E2E_TEST_ROUTES=1; playwright.config boots a second admin-approval
  instance on :3100 and adds setup/chromium/smoke/admin projects.
- Authed specs reuse one storageState (auth.setup.ts) and reset data before
  each test via an auto fixture (fixtures/test.ts); the 5 domain specs drop
  the per-spec register and navigate straight in.
- admin.spec.ts covers the approval flow + invite mint/reveal/revoke.

The gate is E2E_TEST_ROUTES alone, not NODE_ENV: the harness boots via
`next start` (NODE_ENV=production, like a real deploy), so a NODE_ENV check
couldn't distinguish them and would make the route permanently dead.

Smoke + network specs were adjusted for landing/read-mode changes that
landed just before the per-domain specs (#373 Chat landing, #371 detail
read mode), which had left those assertions stale on main.

Closes #326
Refs #150

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.4% ≥ 50%
Branches 71.3% ⚠️ ≥ 75%
Functions 80.6% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.4% ✅ | ≥ 50% | | Branches | 71.3% ⚠️ | ≥ 75% | | Functions | 80.6% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit a8f904f775 into main 2026-06-29 16:45:09 +00:00
james deleted branch test/e2e-infra-hardening 2026-06-29 16:45:09 +00:00
Sign in to join this conversation.
No description provided.