Files
rfc-app/docs/design/2026-06-07-deployed-env-e2e-harness.md
T
Ben Stull 83eafe72ee feat(e2e): deployed-environment E2E harness + gated test-auth (v0.52.0)
The §9 pipeline's PPE+E2E stage was unreachable: e2e/metadata.spec.js was
bound to Tier-1-only scaffolding (docker-seeded faceted collection,
SQLite-injected owner, Mailpit OTC sink). This makes the same suite run
against a deployed host.

- backend: POST /auth/test/login — fail-closed, secret-gated, single-
  identity owner test-login (404 unless E2E_TEST_AUTH_SECRET +
  E2E_TEST_AUTH_EMAIL both set; constant-time compare; loud startup warn).
  6 vertical tests; backend 665 green. Documented in backend/.env.example.
- e2e/lib/auth.js: branch on E2E_TEST_AUTH_SECRET (deployed test-login vs
  local Mailpit OTC); OWNER_EMAIL from E2E_OWNER_EMAIL. Spec unchanged so
  the localhost Tier-1 path keeps working.
- testing/seed-ppe.sh: seed a dedicated, prod-untouching PPE registry +
  content repo (faceted bdd collection) — real OHM content never touched.
- docs/design/2026-06-07-deployed-env-e2e-harness.md; CHANGELOG; VERSION
  + frontend/package.json -> 0.52.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:35:53 -07:00

122 lines
5.7 KiB
Markdown

# Deployed-environment E2E harness (PPE)
**Date:** 2026-06-07 · **Version:** v0.52.0 · **Status:** implemented
## Why
The §9 deployment pipeline is `localhost + E2E → PPE + E2E → prod`. The
middle stage — running the Playwright E2E suite against a *deployed*
pre-prod host (`https://rfc-ppe.wiggleverse.org`) — was unreachable
because the suite (`e2e/metadata.spec.js`, SLICE-3/4/5 of the
configurable-collection-metadata work) was bound to three scaffolds that
exist only in the local Tier-1 docker stack:
1. **A faceted `bdd` collection**, seeded into a throwaway Gitea by
`testing/seed-gitea.sh`.
2. **A granted-owner identity** (`e2e-owner@example.test`), injected
directly into SQLite by the docker-compose `backend-seed` step.
3. **Mailpit**, the SMTP sink the OTC sign-in reads the one-time code
from.
PPE has none of these: it runs against the real `git.wiggleverse.org`
(shared with prod), has no direct DB access, and has no mail sink. This
note records how each coupling is replaced so the *same* spec runs green
against both localhost and PPE.
## The three seams
### 1. Auth — a gated test-login endpoint (the framework change)
A new backend route, `POST /auth/test/login`, replaces both the Mailpit
OTC dance *and* the SQLite owner injection with one gesture: it mints an
authenticated **owner** session for a single pre-configured identity.
It is the framework's only auth bypass, so it is **fail-closed** and must
never function in production:
- **Off by default.** It returns `404` unless **both**
`E2E_TEST_AUTH_SECRET` and `E2E_TEST_AUTH_EMAIL` are set. A production
deployment sets neither, so the route is invisible and inert.
- **Secret-gated.** The caller must present `E2E_TEST_AUTH_SECRET` in the
`X-Test-Auth-Secret` header, compared in constant time. A wrong/absent
secret returns `404` (it does not advertise the route's existence).
- **Single identity.** It will only mint the one configured
`E2E_TEST_AUTH_EMAIL` (case-insensitive); any other address is `403`.
So an enabled PPE exposes exactly one throwaway owner, with the secret
as the trust boundary.
- **Loud at startup.** When enabled, the app logs a `WARNING` at boot, so
an accidental prod enablement is visible rather than silent.
On success it provision-or-links the row (reusing `otc.provision_or_link_user`),
forces it to `role='owner', permission_state='granted'` (the deployed
equivalent of the Tier-1 owner-seed), and stores the session exactly like
the OTC verify path.
**Why an endpoint rather than alternatives.** Reading the OTC code from
the VM's journald (the email adapter logs the envelope to stdout when
SMTP is unconfigured) would couple the test harness to `gcloud` SSH at
runtime — slow, brittle, and operator-cred-bound. Running Mailpit on the
VM and exposing its API publicly is more infra and its own exposure
surface. A default-off, secret-gated endpoint is the portable engineering
seam: it works for *any* deployed environment, needs no SSH, and the
secrets rule (§6.3) is honored — the secret is a Secret Manager ref
injected as VM env, never a literal.
The hard-secrets caveat: the E2E runner presents the secret by resolving
it from Secret Manager at runtime (command substitution), never echoing
it.
### 2. Content — a dedicated PPE registry + content repo
PPE shares the prod Gitea org (`wiggleverse`) and, until now, prod's
registry (`rfc-registry`) and default project (`ohm`). Seeding a faceted
test collection into that shared registry would surface it on **prod**.
So PPE gets its **own**, prod-untouching fixtures:
- `wiggleverse/rfc-registry-ppe` — PPE's project registry. Prod keeps
`rfc-registry`, so prod is never affected.
- `wiggleverse/rfc-app-ppe-content` — one project `ohm` (document) with a
default collection entry plus a faceted `bdd` named collection
(`priority` enum + `tags`) and three entries, mirroring the Tier-1
seed. The E2E path `/p/ohm/c/bdd` therefore resolves identically on
both environments.
PPE is pointed at it with `overlay set rfc-app-ppe
REGISTRY_REPO=rfc-registry-ppe`. The startup reconciler sweep loads the
content into `cached_rfcs` (incl. `meta_json` for facets) on the next
deploy — no webhook needed for the initial load. The seed is scripted in
`testing/seed-ppe.sh` (idempotent; `RESEED=1` restores entry values for a
re-run). Repo *creation* is a one-time operator gesture (the
`write:repository` Keychain token cannot create org repos; create the two
empty repos in the Gitea UI or re-scope the PAT).
### 3. Parameterization — one spec, two environments
- `e2e/playwright.config.js` already honors `BASE_URL`
(default `http://localhost:8080`); PPE sets
`BASE_URL=https://rfc-ppe.wiggleverse.org`.
- `e2e/lib/auth.js` branches on `E2E_TEST_AUTH_SECRET`: set → use
`/auth/test/login`; unset → the original Mailpit OTC path. `OWNER_EMAIL`
reads `E2E_OWNER_EMAIL` (PPE points it at `E2E_TEST_AUTH_EMAIL`) or the
Tier-1 default. The spec itself is unchanged, so the localhost Tier-1
path keeps working.
## PPE version
The harness *requires* the test-login endpoint to exist in the deployed
build, so PPE must run a framework version that contains it — **v0.52.0**,
not v0.51.1. PPE is pinned ahead of prod via its own
`ben/ohm-rfc/.rfc-app-version.ppe` (prod stays on `.rfc-app-version`),
realizing the "PPE stages newer versions first" note the
`deployment.ppe.toml` always anticipated.
## Known limitations
- **Re-runnability.** SLICE-4/5 mutate the seeded entries (commit
sidecars). A clean run needs seed-state preconditions; re-run after
`RESEED=1` + a cache refresh (next reconciler sweep or a redeploy).
Unlike Tier-1's `make e2e-fresh`, PPE has no per-run teardown.
- **smoke.spec.js** stays Tier-1-only (anonymous OTC smoke through
Mailpit); only `metadata.spec.js` runs against PPE.