Skip to content

test(composer): agent-mode DOM + accessibility coverage for the ledger-v2 decision controls - #35

Merged
ryandmonk merged 1 commit into
mainfrom
feat/agent-dom-tests
Aug 4, 2026
Merged

test(composer): agent-mode DOM + accessibility coverage for the ledger-v2 decision controls#35
ryandmonk merged 1 commit into
mainfrom
feat/agent-dom-tests

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

Closes #32.

Every ledger-v2 ownership transition was already pinned in composer-core and the agent routes. Nothing proved a person could reach them: the deployed-composer smoke is agent-free by construction and ships a ledger-v1 demo, so the decision controls never rendered under test. This adds the missing DOM layer above the existing tests — it replaces nothing.

Harness

A third Playwright config, playwright.composer-agent.config.ts:

  • real static export (apps/composer/out, the same artifact that deploys) served by a zero-dependency file server;
  • real local agent (pnpm --filter agent dev) — the actual /project/* routes;
  • real project files: each spec gets its own temp copy of the shipped demo project, and the agent reads and writes it exactly as it does for a user.

No mocked transport and no hand-written ledgers. The only route interception adds latency to a real /project/save (the request still reaches the agent) so the transient busy state is observable instead of raced. One worker, no retries — a retry would replay a decision against already-decided state, which is precisely the flakiness these tests exist to catch.

The premise needs no artificial fixtures: one rediscovery of the demo project produces all three decision families from real source — restructure conflicts (info-card-*, issue #13's exact shape), an id awaiting a decision (a component added to source that a human-owned v1 section cannot attribute), and fresh facts on entries the author owns (a changed doc comment, a new cva variant).

Coverage — 21 specs

Interactions: v1 projects render only the section-level experience; migration surfaces every decision family without deciding any; Restore; Never rediscover; tombstone removal; Keep nested; Restore top-level (nested representation preserved byte-identically); unresolved-conflict persistence across rediscoveries; freshDelta Accept for both ratified shapes (scalar leaf, append-only enum keeping authored order); reload + reconnect persistence; a refused action surfaced verbatim with the ledger byte-unchanged; double activation applying exactly once.

Accessibility: accessible names that identify the component acted on; role="status" live-region announcements; labelled decision groups; keyboard operation and focus never dropping to the body; axe (wcag2a, wcag2aa) over the pending-decision surface, the ownership panel after a decision, and the inventory view.

Product defects the rendered tests uncovered — and fixed

  1. Agent mode was unreachable in the built app. The mount effect tracked loadDemo's identity, which changes when connect() sets extraSurfaces — so connecting to a real project snapped straight back to the demo project. Nothing below the DOM could see this; it is why the first run failed 20/20 at connect. Now a one-shot bootstrap ref.
  2. No control disabled while a decision ran — the ref lock prevented double-apply, but nothing showed it. Decisions now set busy, locking the whole decision surface.
  3. The notice was not a live region — outcomes were painted, never announced.
  4. Ambiguous repeated accessible names ("Restore", "Restore", …). Each control now names its component; the ownership panel's twin controls name their surface.
  5. Decision groups were unlabelled in the accessibility tree.
  6. WCAG AA contrast failures (3.25:1) on the tombstoned entry state, the suppressed report line, and the inventory "bare" chip — --fg-faint on 11px text; now --fg-dim (5.40:1).
  7. Misleading copy: the deletion group asserted "these were deleted from the document", which is false for ids a v1 migration simply could not attribute. Reworded to cover both origins.

Fail-first evidence

The suite failed 10/21 against the pre-fix UI, each failure naming a required behavior (names, live region, groups, axe, busy/disabled, focus). An earlier pass failed 20/20 at connect — which is how defect 1 surfaced. After the fixes: 21/21 green (~29s).

Results

composer-core 23/23 · agent 31/31 · all workspace packages green · typecheck clean · exhibit Playwright 106 passed / 4 pre-existing skips · deployed-composer production smoke 12/12 (unaffected — this branch changes no deployed behavior for the hosted v1 demo beyond the contrast and copy fixes). Wired into CI after the exhibit suite.

Out of scope, per the issue: ledger semantics, casualty acknowledgement (#30), Phase 3 AI, hosted bindings, visual redesign, unrelated test cleanup.

🤖 Generated with Claude Code

Closes the verification gap recorded as #32: every ledger-v2 transition
was pinned in composer-core and the agent routes, but nothing proved a
PERSON could reach them. The deployed-composer smoke is agent-free and
ships a v1 demo, so the decision controls never rendered under test.

Harness (a third Playwright config, playwright.composer-agent.config.ts):
the real static export of apps/composer/out + the real local agent + a
per-spec temp copy of the shipped demo project on disk. No mocked
transport; the only route interception adds latency to a real save so
the transient busy state is observable. One worker, no retries — a retry
would replay a decision against already-decided state.

Coverage (21 specs): v1 stays section-level; migration surfaces every
decision family without deciding any; Restore; Never rediscover;
tombstone removal; Keep nested; Restore top-level; unresolved-conflict
persistence; freshDelta Accept for both ratified shapes (scalar leaf and
append-only enum); reload+reconnect persistence; a refused action
surfaced verbatim with the ledger untouched; double activation applying
exactly once; keyboard operation and focus; accessible names; live-region
announcements; labelled decision groups; axe over the report, ownership
panel, and inventory.

Product defects the rendered tests uncovered, and fixed:
- AGENT MODE WAS UNREACHABLE in the built app: the mount effect tracked
  loadDemo's identity, which changes when connect() sets extraSurfaces,
  so connecting to a real project snapped straight back to the demo.
  Now a one-shot bootstrap ref.
- decisions never set `busy`, so no control disabled while one ran (the
  ref lock prevented double-apply, but nothing showed it)
- the notice was not a live region: outcomes were painted, not announced
- decision buttons carried ambiguous repeated names ("Restore",
  "Restore"); each now names its component, and the ownership panel's
  twins name their surface
- decision groups were unlabelled in the accessibility tree
- WCAG AA contrast failures (3.25:1) on the tombstoned entry state, the
  suppressed report line, and the inventory "bare" chip — --fg-faint on
  11px text; now --fg-dim (5.40:1)
- deletion-group copy asserted "these were deleted", which is false for
  ids a v1 migration could not attribute; reworded to cover both origins

Fail-first: the suite failed 10/21 against the pre-fix UI, each failure
naming a required behavior (names, live region, groups, axe, busy,
focus); an earlier pass failed 20/20 at connect, which is how the
demo-revert defect surfaced. Wired into CI after the exhibit suite.
Workspace: composer-core 23/23, agent 31/31, all packages green,
typecheck clean, exhibit Playwright 106 passed/4 pre-existing skips,
deployed-composer smoke 12/12 unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an agent-mode, DOM-level Playwright harness to exercise the composer’s ledger-v2 decision controls (including keyboard + axe coverage) against the real static export, the real local agent, and real on-disk project files—closing the existing gap where these controls were pinned only at unit/route layers.

Changes:

  • Introduces a dedicated Playwright config + webServer setup for “composer agent-mode” tests, and wires it into CI.
  • Adds an agent-mode test project helper that clones the shipped demo project into a per-spec temp workspace and drives connect/rediscover flows.
  • Updates composer UI state and views to support accessible, testable decision controls (busy/disabled state, live-region notice, labelled decision groups, clearer button names, contrast/copy tweaks).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
playwright.config.ts Excludes the new agent-mode specs from the default Playwright run.
playwright.composer-agent.config.ts New Playwright config to run agent-mode composer suites with two web servers (static export + agent).
e2e/support/agent-project.ts Helper to provision per-test temp projects and drive connect/rediscover flows in the UI.
e2e/serve-composer.mjs New zero-dependency static server for apps/composer/out used by the agent-mode suite.
e2e/composer-agent.spec.ts New end-to-end decision-control interaction coverage (restore/tombstone/conflicts/fresh facts/persistence/double-activation/focus).
e2e/composer-agent-a11y.spec.ts New accessibility coverage (names, live region, labelled groups, axe checks, busy/disabled semantics).
apps/composer/app/views/project-view.tsx Renders decision controls with labelled groups + aria-labels; focus-management after decisions; contrast/copy updates.
apps/composer/app/views/inventory-view.tsx Contrast fix for the “bare” chip state.
apps/composer/app/state.tsx Fixes demo bootstrap re-run; introduces busy state during decisions for UI lockout.
apps/composer/app/composer.tsx Makes notice a live region (role=status, aria-live=polite).
.github/workflows/ci.yml Builds composer static export and runs the new agent-mode Playwright config in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread e2e/serve-composer.mjs
Comment on lines +31 to +40
let path = normalize(decodeURIComponent((req.url ?? "/").split("?")[0])).replace(/^([/\\])+/, "");
if (path === "" || path.endsWith("/")) path += "index.html";
let file;
try {
file = await readFile(join(root, path));
} catch {
file = await readFile(join(root, `${path}.html`)).catch(() => readFile(join(root, "index.html")));
}
res.writeHead(200, { "content-type": MIME[extname(path)] ?? "application/octet-stream" });
res.end(file);
Comment on lines +56 to +61
export function demoProject(options: DemoOptions = {}): DemoProject {
const root = mkdtempSync(join(tmpdir(), "composer-agent-"));
cpSync(DEMO, root, { recursive: true });
const contractPath = join(root, "acme-ui.dspack.json");
const read = () => JSON.parse(readFileSync(contractPath, "utf8")) as Record<string, any>;
const write = (doc: Record<string, any>) => writeFileSync(contractPath, `${JSON.stringify(doc, null, 2)}\n`);
@ryandmonk
ryandmonk merged commit ab5030f into main Aug 4, 2026
2 checks passed
@ryandmonk
ryandmonk deleted the feat/agent-dom-tests branch August 4, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent-mode DOM-level tests for the ledger-v2 decision controls (incl. a11y)

2 participants