Web client selftest¶
The web-driver-selftest is the acceptance proof for the embedded browser web
client (web/, #1592 Phase 5) — the browser analogue of the
TUI driver selftest. It drives the daemon's embedded
single-page app in a headless Chromium against a real af daemon and asserts
the core flows end to end. Assertions are the gate, not screenshots.
Running it¶
That is the only sanctioned entry point. It:
- Builds a dedicated container image
(
scripts/container/Dockerfile.web-selftest) carrying three toolchains the plain testbox image deliberately omits: Go (to buildafand run the daemon), Node (to run Playwright), and a real Chromium with all its system deps. It is pinned to the Playwright version locked inweb/package-lock.jsonso the bundled browser matches the npm package. - Runs the whole harness inside one ephemeral container
(
scripts/container/web-selftest-entry.sh): buildsaf, brings up a real daemon on a throwaway home with a loopback plain-HTTP+token listener (listen_addr=127.0.0.1:8899), seeds two sessions in a mock repo behind a fake agent, then runs Playwright (web/selftest/web-driver.spec.ts).
Everything — the daemon, its tmux server, the sessions, the browser — lives on
127.0.0.1 inside the container and dies with it. There are no published
ports, no access to the host tmux server, and no touch of the real
~/.agent-factory, exactly like the other container harnesses.
In CI¶
Since #2069 a PR touching web/, daemon/, agentproto/, apiproto/, the
harness itself or the CI wiring around it also runs this on a GitHub runner
(.github/workflows/web-selftest.yml), using the same make target — CI does
not keep its own copy of the steps.
Since #2762 it gates, and it runs on master:
- On a PR
pr.ymlcalls the workflow as a job and lists it inBuild'sneeds, so a red run turns a required check red. A cheapWeb selftest scopejob decides first whether the change can reach the web client — the decision lives in.github/scripts/web-selftest-scope.js, whose tests run inLint— and the expensive job isskippedwhen it cannot. Every way of failing to work out the diff resolves to run it. - On master a
pushtrigger runs the same definition with the same path filter. It blocks nothing (master has already merged); its job is to name the commit. Before #2762 nothing ran the suite on master at all, so a regression stayed invisible until some later PR happened to touch these paths — and then it looked like that PR's fault. Master runs are not cancelled by a newer push, so each commit keeps its own verdict.
A red run is worth investigating rather than re-running: twice on 2026-07-21 a "flake" here was a genuine web-client race (#2311, #2341).
The deterministic web checks — typecheck, the unit/parity suites, and
web/dist reproducibility — gate too, as pr.yml's Web job. The two are a pair:
Web proves the committed bundle matches web/src, and this suite proves that
bundle works. This harness deliberately serves the committed web/dist rather
than rebuilding, so it exercises the artifact a released binary embeds — which is
also why you run make web-build before it locally.
Failures give you Playwright's assertion output in the job log but no trace: the
harness works inside the container's own copy of the tree, so its
web/test-results/ never reaches the host. Reproduce locally for a trace.
What it asserts¶
Against the live SPA served over the daemon's plain-HTTP listener:
| Flow | Assertion |
|---|---|
| Login | Pasting the daemon token into the login form renders the authed app. |
| Sidebar | The rail lists the seeded sessions from the Snapshot/events plane, and the client reports an open event stream (.af-app[data-live="open"]). |
| Attach | Click-to-attach opens the xterm terminal and shows the fake agent's live output (a real binary PTY frame decoded by the TS codec and painted in the browser). |
| Keyboard (#1694/#2517) | In the sessions view's rail mode j/k navigate the rail; Enter attaches the selection; ctrl+] returns to the rail; Escape forwards to the agent as its interrupt (the ESC byte reaches the PTY and focus stays in the terminal). |
| View cycling (#1694/PR8) | In rail mode ] cycles the top-level view forward (sessions → tasks) and [ cycles it back (tasks → sessions), the active view tab following each step. |
| Tabs (#1592 PR7) | The tab bar creates a shell tab (+ / t), switches to it (click / 1-9) and shows its distinct PTY output, and closes it (× / w) — the agent tab stays unclosable. |
| Create | The + New modal creates a session and its row appears in the rail. |
| Kill | The kill confirm removes the session's row. |
| Archive | The archive confirm moves a session into the archived group. |
Toolchain boundary¶
Node and Playwright stay entirely behind make web-selftest-container (and
make web-build / make web-test). go build ./... and make test-container
never invoke them — the built web/dist/ is committed, so the Go side is
Node-free (the locked toolchain decision). The harness tests the committed
web/dist/ bundle the binary embeds; rebuild it with make web-build after
changing web/src/.
Artifacts¶
The harness is assertion-gated, so it needs no committed artifacts. Per-run
Playwright outputs (web/test-results/, web/playwright-report/,
web/blob-report/, web/selftest/.last-run.json) are git-ignored; a failing run
retains a trace under web/test-results/ for local debugging.