# 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.