83eafe72ee
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>
122 lines
5.7 KiB
Markdown
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.
|