# DRAFT: IPZ E2E skill update

Audience: AI coding agents first.

Owner must paste the replacement sections below into the gate-protected IPZ skill. This file is a draft only; do not edit the canonical plugin tree from the k3s runner worktree.

## Canonical wrappers and commands

Choose exactly one lane before each run. Both lanes are remote-only. Never mix lanes or silently fall back between them.

- **SSH lane (default):** use when k3s is not explicitly requested, when repairing a per-buildbox snapshot, or when validating the unchanged legacy path. `e2e-remote` uses the registry's `e2e` order and runs server plus browser on one authorized Debian buildbox. Do not pin a host for a normal run; let the registry select and spill.
- **k3s lane (`--k8s`):** use when the k3s Job path is available, the immutable runner image is imported on an eligible node, and dispatcher admission confirms node readiness, the node-local mirror, and the snapshot. The run-owned Job owns stack provisioning, readiness, Playwright, artifacts, and teardown.
- **No fallback:** a failed k3s run remains a k3s failure. Diagnose and rerun with `--k8s`, or deliberately start a separate SSH-lane run. Never retry locally, proxy the target, or switch lanes implicitly.

### SSH lane: canonical command

Run from the plugin's `tests/e2e` directory (standing harness workaround until commit `0d452e4da` lands), so `--server "./remote-stack.sh"` resolves to `tests/e2e/remote-stack.sh`. Use the unchanged SSH-backed pair, exact loopback target, exact absolute pnpm path, and both browser projects.

```bash
cd /path/to/international-press-zone/tests/e2e

~/.claude/bin/e2e-remote \
  --server "./remote-stack.sh" \
  --wait-port 8080 \
  --env WP_BASE_URL=http://127.0.0.1:8080 \
  -- /home/user/.local/share/mise/installs/pnpm/11.5.2/pnpm \
    --dir . \
    exec playwright test --config=playwright.config.js <focused-spec> \
    --project=chromium --project=firefox
```

For a missing or incompatible per-host snapshot, repair it through the same wrapper on a host name emitted by the registry, then rerun the SSH command. This is the only normal repair that pins a host.

```bash
~/.claude/bin/e2e-remote --hosts <registry-emitted-box> -- ./build-e2e-snapshot.sh
```

`<registry-emitted-box>` MUST be a name emitted by the registry. Never replace it with an address or direct SSH command.

### k3s lane: canonical command

Run from the plugin's `tests/e2e` directory, same as the SSH lane. Opt into the run-owned k3s Job. The Job owns server startup and node selection; the client argv contains only the Playwright invocation and its environment. The runner image supplies `pnpm`, and the Job runs the client from the mirror's plugin `tests/e2e` directory inside the Pod, so `--dir .` resolves identically in both lanes.

```bash
cd /path/to/international-press-zone/tests/e2e

~/.claude/bin/e2e-remote \
  --k8s \
  --env WP_BASE_URL=http://127.0.0.1:8080 \
  -- pnpm \
    --dir . \
    exec playwright test --config=playwright.config.js <focused-spec> \
    --project=chromium --project=firefox
```

Do not add SSH-lane `--server`, `--wait-port`, or normal-run `--hosts` options to this command. Do not use `sh -lc`, Bun, `npx`, `npm exec`, or a direct `e2e-k8s-dispatch` invocation in the client seam. The `--k8s` seam derives and validates dispatcher-only values: sanitized `IPZ_E2E_RUN_ID`, `IPZ_E2E_MIRROR_SLUG`, selected node, imported runner image, control-plane endpoint, and receipt/artifact paths. Never pass these internal values as caller `--env` values or treat them as public command flags.

## Failure ladder

Stop at the first failing rung. Preserve selected topology. The dispatcher writes the existing one-line receipt, `outcome<TAB>status<NEWLINE>`, and maps pre-client Kubernetes admission to the public `e2e-remote` ladder. Once the Playwright wrapper emits `K8S_RESULT client <status>`, that marker is authoritative; a client status numerically equal to `4`, `5`, or `97` is never reinterpreted as admission.

| Public status | k3s meaning | Pod/Job state and action |
|---|---|---|
| `3` | Run slot or ownership conflict before admission | No Pod for this run is admitted and no run-owned Job mutation is allowed. Preserve the other run, wait for its label-scoped cleanup, verify this run has no resources, then rerun the same lane. |
| `4` | Server/stack died before listening, or setup/admission failed | Job may exist, but the Pod failed before a client marker: apply/control-plane failure, missing Job/Pod after submission, image/config/hostPath failure, snapshot or repo-delivery init failure, or WordPress/MariaDB/provisioning failure. Inspect Job and provisioning logs; fix prerequisites. |
| `5` | Server never became usable before the deadline | Pod stayed Pending/Unschedulable, node admission did not complete, readiness did not finish, or Job reached `DeadlineExceeded` (`activeDeadlineSeconds`). Fix scheduling/readiness; do not proxy around it. |
| `97` | No usable k3s node | Auto admission exhausted eligible nodes because node readiness, mirror presence, or imported runner-image checks failed. No test result exists; repair node/image/mirror admission. |
| any client status | Playwright/client result | A completed Pod emitted `K8S_RESULT client <status>`. Preserve `<status>` exactly, including client statuses numerically equal to `4`, `5`, or `97`; do not spill them as admission failures. |

When an outer SSH-compatible fleet wrapper handles the receipt, admission `3`, `4`, and `5` become internal retry statuses `83`, `84`, and `85` (`+80`); fleet normalization returns public `3`, `4`, and `5`. Admission `97` is the no-usable-node boundary. Raw client statuses never spill merely because they equal a reserved number.

Apply this ordered ladder:

1. **Concurrent port/slot conflict:** k3s has no admitted Pod for this run. Fail before Job mutation, preserve the other run, wait for owner cleanup, verify this run's label is absent, then rerun the canonical command.
2. **Missing or incompatible snapshot:** snapshot-validation init fails before WordPress or Playwright starts. The Pod fails before the client marker; return admission `4`. Repair the snapshot through the documented SSH wrapper on its receiving host. k3s does not edit or accept a stale fingerprint.
3. **Stack fails before HTTP readiness:** database/WordPress provisioning or readiness fails, or rewrite/REST JSON readiness never completes. The Pod is Failed or never Ready before the client marker; return admission `4` (or `5` only for the explicit deadline/readiness-timeout path). Inspect Job logs only.
4. **`No tests found`:** Playwright started and emitted a client marker with its exact nonzero status. Preserve that client status. Verify the spec exists under `tests/e2e` without changing k3s topology.
5. **Zero group disclosures with a valid REST fixture:** this is a client/runtime failure after Pod admission. Prove delivered mirror, manifest, browser-loaded bundle, and console output; preserve the marked client status. Never convert fixture success into an SPA-build pass.
6. **Missing or invalid admin manifest/assets:** repo-delivery or asset validation fails in init/provisioning before the Playwright marker. The Pod is Failed; return admission `4`. Fix remote lockfile/build/delivery input. Never locally build or commit generated `admin/dist`.
7. **Skip, fixture, graph, or REST-routing failure:** missing fixture, wrong ACF graph, non-JSON `/wp-json/`, or rewrite failure is fail-closed. A pre-client failure maps to admission `4` or explicit timeout `5`; a Playwright assertion/skip result after client start remains the marked client status. Never add a skip, conditional pass, alternate host, or proxy.
8. **Runtime failure:** Playwright exits with a marked nonzero client status, or Pod fails after client start. Extract artifacts while the artifact helper is live, inspect browser/server output, and rerun both projects through the same explicitly selected lane. Do not claim success from a partial report.

## Artifacts and fail-closed cleanup

Artifact paths are unchanged:

- `tests/e2e/test-results/` — traces, screenshots, videos, browser output, and `junit.xml` at `tests/e2e/test-results/junit.xml`.
- `tests/e2e/playwright-report/` — distinct HTML report; never use the raw-artifact directory as the report directory.

The k3s artifact extractor MUST copy only these two fixed paths from the live run-owned artifact helper before Job/Pod deletion. Preserve repository-boundary exclusions: `node_modules`, `.git`, `.cache`, and `target`. Never accept an arbitrary Pod path or write outside the invoking checkout.

Every Job and Pod carries `app=ipz-e2e` plus exact run ownership label `run=<RUN_ID>` in namespace `ipz-e2e`. The dispatcher claims cleanup ownership before apply, deletes only the matching Job with label-scoped, wait-for-completion cleanup on every exit and signal, then re-lists the same selector. Any apply uncertainty, cleanup/removal error, or surviving run-labeled resource is a failure, not a test result; force an admission-failure receipt. Never use an unlabeled bulk delete, namespace deletion, or another run's label.

The Job sets `ttlSecondsAfterFinished: 300` (five minutes) as a Kubernetes garbage-collection backstop. TTL does not replace artifact extraction or explicit label-scoped cleanup: extract first, clean up on every path, and treat TTL as last-resort protection for an already-finished run. TTL MUST never clean another run or substitute for a failed cleanup check.
