Files
openworker/surfaces/gui/e2e
Devika Verma c958d6f262 Step 2: Auto-Approve mode - the reviewer, the hook, and the renames
The mode from ocw-context/docs/reviewed-auto-mode.md (rev. 4), v1 scope.

coworker/reviewer.py (new)
- The 8.3 prompt verbatim, cache-shaped: instructions + known world (folders
  and remotes only) + user-message history in the stable prefix; this turn's
  request and ONE action in the suffix.
- parse_verdict: any defect (empty, non-JSON, unknown verdict) -> unsure.
  There is no parse path that results in execution (8.5).
- Reviewer.review never raises: provider errors and timeouts -> unsure.
  Metering counters (checks / verdicts / tokens) for 1.7.
- AGENT_DENY_MESSAGE: the terse, non-diagnostic refusal the agent gets on a
  deny; the full reason goes to the user only (8.4 asymmetry).

coworker/engine.py
- Reviewer consulted ONLY when: attached, mode is AUTO_APPROVE, session
  explicitly attended (unset is_attended counts as NOT attended, so
  automations can never be reviewed), fewer than two denials this turn.
- Consulted ONLY on decisions the gate marked needs_user - hard denies
  never reach it, so it can only turn "ask" into "allow" (1.2).
- One action per request, fired concurrently for all of a turn's escalating
  calls before the sequential authorize loop (8.6): a verdict cannot land
  on the wrong action, and approval cards still reach the human one at a
  time in call order.
- allow -> runs, audited with the reason. deny -> blocked; user event
  carries the full reviewer reason + allow_anyway; agent message carries
  only AGENT_DENY_MESSAGE. unsure -> today's card.
- Reviewer sees the user's words only, extracted mechanically from
  role=user messages - never agent output, never tool results (4.4).

coworker/permissions.py
- Mode.AUTO renamed Mode.BYPASS_APPROVALS ("bypass-approvals"); legacy
  "auto" still parses via _missing_ so configs, saved sessions, and the
  golden decision table are untouched.
- Mode.AUTO_APPROVE ("auto-approve"): gate-identical to INTERACTIVE except
  session grants ("always allow this ...") no longer auto-allow - they
  route to the reviewer instead (1.5: out-of-band standing policy may skip
  the judge; an in-flow click may not). Config allowlists still skip.
- _domain_allowed(include_session=False) checks the user-settings list only.

coworker/config.py: auto_approve flag, off by default, _GLOBAL_ONLY (a
cloned repo cannot hand itself a looser reviewer). agent.py attaches the
Reviewer only when the flag is on; without it AUTO_APPROVE behaves exactly
like INTERACTIVE.

server/manager.py: autonomy audit ranks auto-approve above interactive
(turning the reviewer on IS raising autonomy) and below bypass.

GUI: mode picker label "Full access" -> "Bypass approvals" (wire value
"auto" kept). Verified live against the real sidecar; e2e spec updated;
tsc and all 111 GUI unit tests pass.

Tests: tests/test_auto_approve.py (33) - gate behaviour per mode, fail-
closed parsing, prompt shape, deny asymmetry, retry guard, attended
gating, hard-deny isolation, per-action verdict landing, and that the
reviewer never sees agent prose. Permission suites + golden table: 146
passing unchanged.
2026-08-12 12:42:17 -07:00
..
2026-07-21 11:09:41 -07:00
2026-08-01 08:50:53 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00
2026-07-21 11:09:41 -07:00

E2E tests (Playwright)

End-to-end regression tests for the GUI. They drive the real app in Chromium but are hermetic: every /v1 request and the event WebSocket are mocked at the network layer, so tests need no Python backend, run deterministically, and never mutate real state.

Run

npm run e2e          # headless
npm run e2e:ui       # Playwright UI mode (watch/inspect)
npx playwright test e2e/settings.spec.ts   # a single spec

Live smoke (not CI)

npm run e2e:live runs e2e-live/ (separate playwright.live.config.ts) against the real backend on :8765. Two flavors, both skip cleanly when the backend is down:

  • API-shape smoke (api-smoke.spec.ts) — no model tokens, no creds. Asserts /v1/health and /v1/providers return the shapes the GUI reads, catching drift between the mocks and the real backend. Cheap enough to run anytime the sidecar is up.
  • Full vertical (fib.spec.ts, …) — asks a fresh Cowork session to produce fib.md and verifies the file lands on disk. Needs a model configured, is nondeterministic, and costs a few tokens per run. Exercises the vertical the hermetic specs mock: model wiring, the tool/approval loop, file I/O, and WebSocket streaming.

The config (playwright.config.ts) starts the Vite dev server on port 5199 (dedicated, so it won't clash with a running npm run dev on 5173) and reuses it if already up.

How the mock works

e2e/fixtures.ts exports a test whose page has mockApi() installed before navigation:

  • page.route("**/v1/**", …) dispatches by pathname + method to fixtures whose shapes mirror the real backend (captured from a live server). Unknown endpoints return an empty-but-valid body.
  • Mutations are held in per-test in-memory state so they reflect through the real UI on re-fetch: sessions (archive/rename/delete), personas (enable/surface/delete — enable implies surface, matching the backend), inbox items + the routing binding, roots, channel subscriptions.
  • The session WebSocket (routeWebSocket) is a scripted fake agent speaking the real {type, data} event protocol: ready on connect; user_messageturn_start → deltas → assistant_message "Echo: <text>"turn_done; a message containing "run a tool" emits tool_proposed + permission_required and suspends until the client's approval decision arrives. This runs the production send/stream/approve code paths with zero model cost.
  • Seed data worth knowing: the pinned session "Draft the launch note" is the newest (boot-resume target); 7 unpinned "Weekly plan N" cowork sessions exercise the sidebar peek cap; two pending Inbox items (approval on cowork, question on ops) drive the Inbox filters; acme-notes is a disabled non-builtin persona for enable/delete flows. Providers are seeded in three states (OpenAI configured+used, Anthropic configured-unused, Z AI unconfigured w/ prefilled endpoint) — POST /v1/providers flips configured on save, /verify fails on a key containing "bad". One automation ("Daily AI News") with a running run — POST .../run appends a run, PATCH/DELETE toggle and remove.

Adding a spec

import { test, expect } from "./fixtures";

test("…", async ({ page }) => {
  await page.goto("/");
  // interact + assert
});

If a flow reads a new endpoint, add its fixture + a route branch in fixtures.ts — the catch-all returns {}, which will crash components that expect arrays (e.g. persona recommends). Prefer getByRole, but note some controls (the Sources bar, the ✕ remove) take their accessible name from inner content — target those with getByTitle/getByLabel.