A round in which every lens returned null read as a clean pass and would now have been quoted verbatim into the PR body; it is an error before the push, in the review loop and in the post-rebase round, which also gains the same blocking-or-should-fix filter; a fixer that dies or stops (no done) is an error too, the same test the implementer already gets. The history entry now carries the fix that answered that round and only the commits that fix added (a line-set difference against the previous branch log — a fixer that reformats or rebases mid-loop defeats it, which is why the finisher is told to read git show, not the list). An empty fix-commit set is described as "answered without a new commit" when a fix round ran, and as "round one was clean" only when none did. web/scripts/orchestration-workflow-loop.test.mjs compiles the committed script bodies with stubbed agent/parallel and pins eleven paths per script (22 tests). Measured: reverting the loop condition to blocking-only reddens six cases per script (every case that needs a should-fix round to reach the fixer); deleting any of the three zero-lens guards, the fixer guard or its done half, or the empty-fix sentence branch reddens its own case, in both scripts. web/vite.config.ts is untouched: it is pinned whole by test_image_build_delegates_the_spa_suite.py, comments included. Decisions-Edit: yes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
4.7 KiB
key, title, status, since, supersedes, superseded-by, rule, signals, mechanics
| key | title | status | since | supersedes | superseded-by | rule | signals | mechanics |
|---|---|---|---|---|---|---|---|---|
| ci.ui-e2e-harness | 2026-07-25 — UI-E2E: headless Playwright flows in the existing `functional-e2e` job, browser baked into the CI image (#445) | active | 2026-07-25 | none | none | The UI-interactive E2E flows run as headless Playwright specs (`web/e2e/*.spec.ts`, driven by `scripts/e2e-ui.sh`) in a **second step of the existing advisory `functional-e2e` job**, never their own job; the browser is `chromium-headless-shell` **baked into the CI toolchain image** (`docker/ci/Dockerfile`, `PLAYWRIGHT_VERSION` kept equal to `web/package.json`'s EXACT `@playwright/test` pin), never installed per run; specs are `serial` with `retries: 0` and assert only contracts the curl harness structurally cannot reach. | UI-E2E, Playwright headless, boot-gate/Setup/Login flows, chromium-headless-shell, PLAYWRIGHT_BROWSERS_PATH=/ms-playwright, browser baked not installed, e2e-ui.sh, vitest `e2e/**` exclude, port 8410, no retries · paths: `scripts/e2e-ui.sh`, `web/playwright.config.ts`, `web/e2e/boot-gate.spec.ts`, `docker/ci/Dockerfile`, `.gitea/workflows/docker-build.yml`, `docs/e2e-local.md` · issues: #445, #363, #299 | `scripts/e2e-ui.sh`; `docs/e2e-local.md` -> "UI-E2E harness"; `docs/ci-cd.md` -> "CI toolchain image" |
Completes the last #299/#363 follow-up — the UI-interactive flows both prior records deferred.
Only assert what curl structurally cannot. The curl harness already covers the auth HTTP contracts
(setup-claim 200/409, login 401/200, CSRF 403, stamp rotation); re-asserting them through a browser buys
nothing but flake surface. Scope is the four things curl cannot express: client-side form validation
(the Setup confirm-password gate is pure React state, makes no request), AuthGate's rendered states,
the session cookie authenticating the SPA's own /api XHRs (curl proves the cookie works for curl,
not that the app sends it), and sign-out via UserMenu. This scoping rule is the durable part —
extend the browser suite only when a contract fails that test.
Baked browser, not a per-run install. chromium-headless-shell lands in /ms-playwright at image
build time, so the job installs nothing — the "jobs install nothing at run time" rule of the shared CI
toolchain image (#390, which has no standalone record — see ci.runner-placement and docs/ci-cd.md).
Measured on the real base: headless shell 267M vs full chromium 656M (+171M compressed pull),
and chromium.launch() resolves to the shell anyway; the tradeoff is that a headed run in the image
fails. Verified rather than assumed: Chromium runs as root in-container with no --no-sandbox
opt-out. The browser revision is tied to the npm package version, so that pin is EXACT and e2e-ui.sh
guards drift by launching a browser — not by path, since executablePath() reports the
full-chromium path such an image lacks.
Why that job and not a new one (docs/ci-cd.md has the detail): its npm ci + Release build are
already done, so a separate job would duplicate the dominant cost to add ~5s of browser work. The UI step
boots its own fresh instance on port 8410 — the first spec asserts the one-shot Setup gate the curl
step has already claimed.
serial + retries: 0, and vitest must not collect these files. Server state is shared and partly
one-shot (the setup-claim), so specs are serial/single-worker; each test still gets its own browser
context, which gives the login specs a signed-out browser without a logout dance. Retries are 0 even in
CI — a retry lets a flaky flow merge looking green. Coupling worth knowing: vitest's default include
glob would run web/e2e/*.spec.ts under jsdom, so vite.config.ts excludes e2e/** by spreading
configDefaults.exclude — not by narrowing include to src/**, which would silently stop collecting
the real vitest tests under web/scripts/.
Lifecycle correctness in scripts/e2e-ui.sh — all four found by adversarial review, none by a
passing run; that is the transferable lesson (green runs never exercise the failure/interrupt paths).
(a) Install the cleanup trap before boot and have e2e-local.sh publish its PID to an opt-in
ETV_PIDFILE as it forks, so a mid-boot signal can still reap the server — a "kill whatever LISTENS on
the port" fallback is wrong twice over: it needs lsof (absent from the CI image) and it infers
ownership instead of proving it. (b) Re-raise signals, so a cancelled run cannot exit 0. (c) Never
if ! cmd; then status=$? — under ! bash sets $? to the logical negation, so it reads 0 and a
FAILING run exits 0, silently passing CI. (d) exec the app inside the backgrounded subshell, or $!
is the SUBSHELL on bash 3.2 (stock macOS) and every PID-based kill targets the wrong process.