# Test harness (handbook §10.3 two-tier testing) One environment-agnostic suite, two targets. ## Tier 1 — local Docker (every PR) ```sh make e2e-install # one-time: install Playwright + chromium (cold checkout) make tier1-up # build + start: gitea(seeded) + backend + web(nginx) + mailpit make e2e # run Playwright against http://localhost:8080 make fe-unit # run Vitest frontend unit tests make tier1-down # stop + wipe volumes ``` - App (SPA + API): http://localhost:8080 - Mailpit UI / API: http://localhost:8025 - Gitea (disposable): http://localhost:3001 The stack is hermetic and disposable — fresh SQLite + fresh seeded Gitea each `tier1-up`. e2e signs in via the email OTC flow, reading the code back from Mailpit, so no real OAuth provider is needed. **CI note:** the backend enforces a per-IP OTC request limiter (5 requests / 300s, `backend/app/ratelimit.py`). A single `make e2e` run uses exactly one OTC request, so the normal "fresh `tier1-up` then one `e2e`" flow is well clear of it. Do not retry `make e2e` more than ~4 times in a 5-minute window against the same running stack, or the 6th OTC request will 429. Restarting the backend container (or `tier1-down`/`tier1-up`) resets the in-process limiter. Note: this is distinct from the per-email 60s OTC cooldown (`OTC_REQUEST_COOLDOWN_SECONDS`, `backend/app/otc.py`); the smoke spec handles that separately by using a unique email address per run. ## Tier 2 — PPE (deploy gate) The SAME suite, pointed at the PPE instance (once `rfc-app-ppe.` is stood up via flotilla — see the engineering handbook §10.1/§10.3): ```sh cd e2e && BASE_URL=https://rfc-app-ppe. MAILSINK_URL= npm run e2e ``` PPE provides the real nginx/systemd/SQLite topology + its own isolated Gitea + always-pass Turnstile keys. Standing up the PPE VM is an operator task, not part of this repo.