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