@systemproof/driver
v0.1.0
Published
Integrated browser plane for the QA harness: the `ExploreDriver` (LLM-driven observe→act loop) and `ReplayDriver` (book-grounded deterministic replay) over Playwright, plus the skills-facing driver CLI (`session start|observe|act|stop`).
Readme
@systemproof/driver
Integrated browser plane for the QA harness: the ExploreDriver (LLM-driven
observe→act loop) and ReplayDriver (book-grounded deterministic replay) over
Playwright, plus the skills-facing driver CLI (session start|observe|act|stop).
Shipped (P1.1) — ExploreDriver
An agent-facing driver over a closed op-set (no raw coordinates, no raw CDP), so P3.4's agent can only ever take one of a fixed set of actions.
import { createExploreDriver } from '@systemproof/driver';
const driver = createExploreDriver();
const session = await driver.start({ baseUrl: 'http://localhost:3000' });
const obs = await driver.observe(session); // {url, title, ariaSnapshot, refs, screenshotPath, consoleTail, networkTail}
await driver.act(session, { kind: 'click', ref: 'e8' });
await driver.stop(session);observereturns a ref-annotated aria snapshot viapage.ariaSnapshot({ mode: 'ai' })— the public Playwright 1.59 API that emits[ref=eNN]element references. Each ref resolves through thearia-ref=selector engine and is element-identity-stable: it survives DOM mutations that shift positional/nth indices. A screenshot is always written under the session tmp dir;consoleTail/networkTailare ring buffers drained per call.acttakes theOpunion (click/fill/type/select/press/navigate/scroll/waitState/back). Failures return a structuredActResultcarrying{ code, retryable }(REF_STALE,REF_NOT_FOUND,NAV_TIMEOUT,BLOCKED_DIALOG,PAGE_CRASH).- Auth —
authProfileloads a PlaywrightstorageStatefrom<fixturesDir>/auth/<profile>.json;saveAuth(session, path)captures the current state.profileDiruses a dedicated persistent profile instead. waitStatereuses theAnchortype from@systemproof/core(the qa-core book contract), never redefining it.
fixtures/site/ + src/explore/testServer.ts are the bundled fixture app and
in-test static server used by the browser integration suite.
Shipped (P1.2) — driver CLI with cross-process sessions
A qa-driver bin (run via tsx) so a skill can session start in one shell
step and observe/act in later, independent steps against the same live
browser.
node --import tsx src/cli.ts session start --base-url http://localhost:3000 # prints <id>
node --import tsx src/cli.ts observe --json # NDJSON observation
node --import tsx src/cli.ts act '{"kind":"navigate","url":"/checkout"}' # persists
node --import tsx src/cli.ts observe --json # sees /checkout
node --import tsx src/cli.ts session stopCommands: session start|stop|list|save-auth, observe [--json],
act <op-json> [--json], screenshot [path]. --session <id> selects; with a
single active session it is optional (ambiguity is an error, never a guess).
How persistence works. session start spawns a detached host process that
owns the browser and outlives the CLI. The host uses
chromium.launchPersistentContext with --remote-debugging-port=0; the op
commands reattach with chromium.connectOverCDP(meta.cdpEndpoint) →
browser.contexts()[0].pages()[0], run the shared explore/page-ops logic,
then disconnect (the host stays up). A plain launchServer+connect context is
per-connection and invisible to a second process — CDP attach is the model
that actually shares one browser. Cross-process, refs resolve by a11y identity
(role+name) rather than aria-ref (whose registry does not survive a reconnect).
Output framing. --json emits NDJSON: one complete JSON object per line,
terminated by a { "type": "end", "ok": ... } sentinel on its own line, with a
drain-before-exit flush — never a partial line.
Session state lives under $QA_DRIVER_HOME/sessions/<id>/ (defaults to
~/.qa-driver; override for isolation).
Shipped (P1.5) — walkthrough recording + auth capture
# Headed: a human drives; the driver records inferred ops into trace.json and
# captures the login as a named auth profile.
node --import tsx src/cli.ts session start --base-url http://localhost:3000 \
--record --headed --save-auth buyer --fixtures-dir ./qa/fixtures
# ... human logs in in the opened window ...
node --import tsx src/cli.ts session save-auth # or `session stop` (deferred save)--recordlogs atrace.json(in the session dir, and to--out <path>) compatible with the explore/codify trace schema (traceEntrySchema): one entry per inferred op — navigations, plus clicks/field-edits resolved to role+name. Field values are never recorded.--save-auth <profile>writes a PlaywrightstorageStateto<fixturesDir>/auth/<profile>.json(reuses the P1.1saveAuthprimitive) onsession save-authorsession stop. Feed it back withsession start --auth <profile>.
Manual auth-capture smoke (human in the loop)
The jest suite includes an automated proxy for this (a second set of CLI acts plays the human, headless). To do the genuine manual smoke:
- Start a headed recording session against a real app:
session start --base-url <app> --record --headed --save-auth me --fixtures-dir <dir>. - In the opened browser window, log in by hand (and click through any flow you want captured).
session save-auth(orsession stop). Confirm<dir>/auth/me.jsonexists and that<session-dir>/trace.jsonlists your navigations/clicks.- Start a fresh session with the profile and confirm it is already logged in (no login wall): `session start --base-url --auth me --fixtures-dir
Later P1 tasks
ReplayDriver (with qa-books, P1.3/P1.4). See
docs/plan/2026-07-16-001-qa-harness-standalone-e2e-system.impl.md.
