The journeys harness: the app stack in Docker, a fake issuer, bot and AI, Playwright in three projects with an axe gate (#726) #746

Merged
claude-bot merged 5 commits from build/journeys-harness into main 2026-09-22 06:31:51 +00:00
Collaborator

The journeys harness (#726): the brief's seven journeys as end-to-end tests, run against the real app stack in Docker, in three projects (desktop, an emulated phone, keyboard-only) with an axe scan on every page. One command locally (make journeys) and one CI job (journeys, after the image build, artefacts on failure).

The stack (docker-compose.e2e.yml, an overlay on the root compose; the bot and beat are never started): the app; a fake OIDC issuer so the real login and callback code runs and each test picks a seeded identity, with no test-only login in the product; Mailpit as the mail catcher; a fake bot that accepts what the backend posts and also consumes the Redis stream, because that is how a campaign with a guild is told things; a fake AI server speaking the bundled WhisperX and llama.cpp shapes, so the real provider adapters run against it. Zero product code changed. .env.e2e is committed with obviously fake values.

The seed creates the prototype's fiction through the API as the signed-in identities: Dana Whitlock runs The Amberwake Accord (with a Discord guild), Priya Raghunathan plays in it, Session 13 is completed, Session 14 is confirmed a week ahead; it proves on the way that product mail reaches the catcher and a session_confirmed event reaches the fake bot. Each test creates what it mutates under a unique name.

Playwright (webapp/frontend/e2e/): JavaScript, since the repo has no TypeScript toolchain and Playwright would strip types without checking them; three projects; signInAs(), openAccount() and axe() fixtures. The keyboard project is enforced, not requested: any pointer call throws, and J0 asserts the guard itself. The axe gate is WCAG 2.2 AA and it fails the run, with an owned baseline (a11y-baseline.js) listing the pre-redesign pages' known violations per page, per element, with the lane that removes them; anything else fails, and the message carries axe's own sentence per element. QB_E2E_STRICT_A11Y=1 ignores the baseline so a lane can ask whether its page is clean yet.

J0, the smoke journey, is written against the new shell (#742): sign in as Dana, land on Home, the campaign in the Contents rail and on the page, the account menu opened with Enter on every project (picked by what is visible, since the phone and desktop controls are different elements), the bell by name, the axe scan open and closed. J1 and J2 are stubbed with their steps as a TODO for lane 1's Home PR to fill in. Nine tests, green in all three projects.

What the gate found on its first day (both filed as #745, tolerated as BUG entries until fixed): lane 0's account menu puts a radio group inside role="menu", and draws the person's own name at 1.14:1 because the panel inherits the dark chrome's ink. Also #743 (campaign create silently drops eight declared fields).

Repo housekeeping the harness needed: .gitattributes forcing LF on shell scripts (a CRLF init.sh stops Postgres on a Windows checkout); PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD in the frontend images and the CI frontend job; vitest's include narrowed to src/ so it stops collecting Playwright specs; the Playwright output directories ignored by ESLint; ruff over e2e/; a gitleaks allowlist for .env.e2e; exact pins for @playwright/test, @axe-core/playwright and the Playwright image, which move together.

The one thing this PR's CI run will answer: whether the Forgejo runner's bind mounts reach the workspace (the synthetic-session job already works around docker-in-docker for the same reason). The script probes it right after up and fails with a message naming the remedy; if it does, the follow-up is to bake e2e/ into the fake and seed images as that job does.

Frontend: lint clean, 1,156 tests, build green. docs: webapp/frontend/e2e/README.md.

🤖 Generated with Claude Code

The journeys harness (#726): the brief's seven journeys as end-to-end tests, run against the real app stack in Docker, in three projects (desktop, an emulated phone, keyboard-only) with an axe scan on every page. One command locally (`make journeys`) and one CI job (`journeys`, after the image build, artefacts on failure). **The stack** (`docker-compose.e2e.yml`, an overlay on the root compose; the bot and beat are never started): the app; a **fake OIDC issuer** so the real login and callback code runs and each test picks a seeded identity, with no test-only login in the product; **Mailpit** as the mail catcher; a **fake bot** that accepts what the backend posts and also consumes the Redis stream, because that is how a campaign with a guild is told things; a **fake AI server** speaking the bundled WhisperX and llama.cpp shapes, so the real provider adapters run against it. Zero product code changed. `.env.e2e` is committed with obviously fake values. **The seed** creates the prototype's fiction through the API as the signed-in identities: Dana Whitlock runs The Amberwake Accord (with a Discord guild), Priya Raghunathan plays in it, Session 13 is completed, Session 14 is confirmed a week ahead; it proves on the way that product mail reaches the catcher and a `session_confirmed` event reaches the fake bot. Each test creates what it mutates under a unique name. **Playwright** (`webapp/frontend/e2e/`): JavaScript, since the repo has no TypeScript toolchain and Playwright would strip types without checking them; three projects; `signInAs()`, `openAccount()` and `axe()` fixtures. The keyboard project is enforced, not requested: any pointer call throws, and J0 asserts the guard itself. **The axe gate is WCAG 2.2 AA and it fails the run**, with an owned baseline (`a11y-baseline.js`) listing the pre-redesign pages' known violations per page, per element, with the lane that removes them; anything else fails, and the message carries axe's own sentence per element. `QB_E2E_STRICT_A11Y=1` ignores the baseline so a lane can ask whether its page is clean yet. **J0, the smoke journey**, is written against the new shell (#742): sign in as Dana, land on Home, the campaign in the Contents rail and on the page, the account menu opened with Enter on every project (picked by what is visible, since the phone and desktop controls are different elements), the bell by name, the axe scan open and closed. J1 and J2 are stubbed with their steps as a TODO for lane 1's Home PR to fill in. Nine tests, green in all three projects. **What the gate found on its first day** (both filed as #745, tolerated as `BUG` entries until fixed): lane 0's account menu puts a radio group inside `role="menu"`, and draws the person's own name at 1.14:1 because the panel inherits the dark chrome's ink. Also #743 (campaign create silently drops eight declared fields). **Repo housekeeping the harness needed:** `.gitattributes` forcing LF on shell scripts (a CRLF `init.sh` stops Postgres on a Windows checkout); `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` in the frontend images and the CI frontend job; vitest's `include` narrowed to `src/` so it stops collecting Playwright specs; the Playwright output directories ignored by ESLint; ruff over `e2e/`; a gitleaks allowlist for `.env.e2e`; exact pins for `@playwright/test`, `@axe-core/playwright` and the Playwright image, which move together. **The one thing this PR's CI run will answer:** whether the Forgejo runner's bind mounts reach the workspace (the `synthetic-session` job already works around docker-in-docker for the same reason). The script probes it right after `up` and fails with a message naming the remedy; if it does, the follow-up is to bake `e2e/` into the fake and seed images as that job does. Frontend: lint clean, 1,156 tests, build green. `docs`: `webapp/frontend/e2e/README.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

First CI run: every other job green; the journeys job failed at docker compose up with questboard-e2e-db-1 exited (126), before the bind-mount probe could run. That is the predicted docker-in-docker case: the runner is a container using the host's daemon, so the checkout's bind-mount sources do not exist on the host and Postgres's entrypoint finds an empty directory where init.sh should be. The fix in progress makes the e2e stack bind-mount-free, the way the synthetic-session job already is: the db init script, the fakes, the seed and the specs baked into their images, with the results copied out for the artefact step.

First CI run: every other job green; the `journeys` job failed at `docker compose up` with `questboard-e2e-db-1 exited (126)`, before the bind-mount probe could run. That is the predicted docker-in-docker case: the runner is a container using the host's daemon, so the checkout's bind-mount sources do not exist on the host and Postgres's entrypoint finds an empty directory where `init.sh` should be. The fix in progress makes the e2e stack bind-mount-free, the way the `synthetic-session` job already is: the db init script, the fakes, the seed and the specs baked into their images, with the results copied out for the artefact step.
rbrooks force-pushed build/journeys-harness from a152a49bd8
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 57s
CI / Bot tests and audit (pull_request) Successful in 1m53s
CI / Bot/backend version sync (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 1m7s
CI / Docker image build (pull_request) Successful in 1m23s
CI / Feedback site tests (pull_request) Successful in 1m30s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m50s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 4m19s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m13s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 17m23s
to d47fa958f5
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m47s
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Bot/backend version sync (pull_request) Successful in 32s
CI / Docker image build (pull_request) Successful in 18s
CI / Feedback site tests (pull_request) Successful in 47s
CI / Bot tests and audit (pull_request) Successful in 1m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m50s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m19s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
2026-09-22 05:33:31 +00:00
Compare
Author
Collaborator

Pushed the rework: the e2e stack mounts nothing from the host. There were nine bind mounts across eight services in the effective config (the db's init script, ./backups on three product services, the e2e/ tree on the four harness services, and the frontend on the runner), so the probe would only have moved the failure. Three Dockerfile stages replace them, following the synthetic-session job's precedent: e2e-db (Postgres with the init script copied in), e2e-fakes (the backend image plus e2e/, for the three fakes and the seed) and e2e-playwright (the pinned Playwright image with npm ci and the specs baked in); ./backups is a named volume. Artefacts come out by docker cp from the runner container before teardown, proven on a deliberately failing run. A new backend test fails if docker-compose.e2e.yml ever grows a host path, or if a service the base file mounts into is not restated with volumes: !override. The local edit loop survives as an opt-in docker-compose.e2e.dev.yml under QB_E2E_DEV=1, which CI never loads.

Also rebased onto #744, so the bell has real data now; that added no violation. The two baseline entries for #745 stay until #747 merges.

Pushed the rework: the e2e stack mounts nothing from the host. There were nine bind mounts across eight services in the effective config (the db's init script, `./backups` on three product services, the `e2e/` tree on the four harness services, and the frontend on the runner), so the probe would only have moved the failure. Three Dockerfile stages replace them, following the `synthetic-session` job's precedent: `e2e-db` (Postgres with the init script copied in), `e2e-fakes` (the backend image plus `e2e/`, for the three fakes and the seed) and `e2e-playwright` (the pinned Playwright image with `npm ci` and the specs baked in); `./backups` is a named volume. Artefacts come out by `docker cp` from the runner container before teardown, proven on a deliberately failing run. A new backend test fails if `docker-compose.e2e.yml` ever grows a host path, or if a service the base file mounts into is not restated with `volumes: !override`. The local edit loop survives as an opt-in `docker-compose.e2e.dev.yml` under `QB_E2E_DEV=1`, which CI never loads. Also rebased onto #744, so the bell has real data now; that added no violation. The two baseline entries for #745 stay until #747 merges.
Author
Collaborator

Second run: the journeys job is green on the runner. The stack came up with no host mount, the seed ran, the nine smoke tests passed in all three projects, and the artefact path works. The remaining jobs are the ordinary ones. Merging on green.

#745 has merged since this branch's last rebase, so the two BUG baseline entries are now stale; they come out with lane 1's Home PR, which removes the /dashboard entries as well when Home replaces the dashboard, and the gate will prove both.

Second run: the `journeys` job is green on the runner. The stack came up with no host mount, the seed ran, the nine smoke tests passed in all three projects, and the artefact path works. The remaining jobs are the ordinary ones. Merging on green. #745 has merged since this branch's last rebase, so the two `BUG` baseline entries are now stale; they come out with lane 1's Home PR, which removes the `/dashboard` entries as well when Home replaces the dashboard, and the gate will prove both.
rbrooks closed this pull request 2026-09-22 05:50:40 +00:00
claude-bot reopened this pull request 2026-09-22 05:51:12 +00:00
rbrooks force-pushed build/journeys-harness from d47fa958f5
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m47s
CI / Backend lint (ruff) (pull_request) Successful in 29s
CI / Bot/backend version sync (pull_request) Successful in 32s
CI / Docker image build (pull_request) Successful in 18s
CI / Feedback site tests (pull_request) Successful in 47s
CI / Bot tests and audit (pull_request) Successful in 1m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m50s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m19s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
to 63259060a3
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 30s
CI / Bot/backend version sync (pull_request) Successful in 46s
CI / Feedback site tests (pull_request) Successful in 52s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m16s
CI / Bot tests and audit (pull_request) Successful in 1m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m26s
CI / Docker image build (pull_request) Successful in 4m43s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 4m40s
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
2026-09-22 05:59:02 +00:00
Compare
Author
Collaborator

Housekeeping: the first merge attempt answered "try again later" while Forgejo was recomputing mergeability after #747 landed, and a chained cleanup step deleted the head branch before that was noticed, which auto-closed the PR. The branch is restored, the PR reopened, and the real cause of the non-mergeable state, a conflict in AppFrame.test.jsx between #747's new identity-block assertion and this branch's surname rename, is resolved by rebasing onto main (the assertion keeps #747's structure with the prototype's "Dana Whitlock"). CI is running on the rebased head; the merge waits for it, with the cleanup as a separate step this time.

Housekeeping: the first merge attempt answered "try again later" while Forgejo was recomputing mergeability after #747 landed, and a chained cleanup step deleted the head branch before that was noticed, which auto-closed the PR. The branch is restored, the PR reopened, and the real cause of the non-mergeable state, a conflict in `AppFrame.test.jsx` between #747's new identity-block assertion and this branch's surname rename, is resolved by rebasing onto main (the assertion keeps #747's structure with the prototype's "Dana Whitlock"). CI is running on the rebased head; the merge waits for it, with the cleanup as a separate step this time.
fix(e2e): the account popover is the menu's box since #745, and Forgejo wants upload-artifact@v3 (#726)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 54s
CI / Feedback site tests (pull_request) Successful in 40s
CI / Backend lint (ruff) (pull_request) Successful in 1m21s
CI / Bot/backend version sync (pull_request) Successful in 34s
CI / Docker image build (pull_request) Successful in 1m6s
CI / Bot tests and audit (pull_request) Successful in 4m32s
CI / Frontend tests, audit, and build (pull_request) Successful in 5m52s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m10s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 13m25s
CI / Backend migration, tests, and audit (pull_request) Successful in 17m17s
babb91f562
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Author
Collaborator

Third run's two failures, both read from the log and fixed: the smoke test looked for Dana's name inside the role="menu" element, which #747 had just moved the identity block out of (the phone project passed because the sheet is different markup), so the account helper now returns the popover's box that holds both the head and the menu panel; and upload-artifact@v4 is not supported on Forgejo, so the artefact steps use v3. Nine of nine green locally in CI mode; CI is running again on the new head. The bind-mount-free stack itself worked on the runner, and the artefacts were found where the docker cp put them.

Third run's two failures, both read from the log and fixed: the smoke test looked for Dana's name inside the `role="menu"` element, which #747 had just moved the identity block out of (the phone project passed because the sheet is different markup), so the account helper now returns the popover's box that holds both the head and the menu panel; and `upload-artifact@v4` is not supported on Forgejo, so the artefact steps use v3. Nine of nine green locally in CI mode; CI is running again on the new head. The bind-mount-free stack itself worked on the runner, and the artefacts were found where the `docker cp` put them.
Sign in to join this conversation.
No description provided.