src/mock/ — the in-browser mock backend
A fake backend that runs in the page, behind the same orval-generated DTOs the real one serves. It exists so the whole PWA can be demoed and driven by an automated frontend test with no FastAPI, no sidecar, no transcripts on disk.
npm run dev:mock # Vite with the mock forced on (VITE_MOCK=1)
# …or, against any build (including the deployed one):
open https://ai-agent.lab.gabvdl.xyz/?mock=1 # sticky until ?mock=0
How it works
installMock() (called from main.tsx before the app tree is imported, since
several modules fetch at import time) swaps two globals:
window.fetch— every same-origin/api/*request is answered byhandlers.tsfrom the in-memory database; anything else falls through to the real fetch.window.EventSource—/api/eventsis served by a tiny hub that emits the same{type: "meta" | "transcript", ids}eventsbackend/events.pydoes, so the app's SSE-driven React Query invalidation runs for real.
| file | role |
|---|---|
enabled.ts |
the ?mock=1 / VITE_MOCK flag (tiny — the only part in the main bundle) |
db.ts |
the database: one object, persisted to localStorage, plus the SSE hub and the detail→summary projections |
seed.ts |
the combinatorial fixture (below) |
handlers.ts |
the routing table — every /api/* route, reads and writes |
sim.ts |
spawn / resume / interrupt: a scripted turn that streams |
index.ts |
installs the interceptors, window.__mock, the MOCK API badge |
Writes really mutate the db (and persist): archiving a conversation, editing a
file, saving settings (the server-backed Zustand stores hit /api/ui-state/*),
uploading an attachment. A spawn or resume actually runs — items are appended
to the conversation one at a time, each publishing a transcript event, until
the run stamps notified + a message ending in DONE and flips to finished.
interrupt drops the rest of the script and records an interruption.
The seed is combinatorial
seed.ts doesn't hand-write a few plausible rows — it enumerates the
cross-product of every field the UI branches on, so every visual state is
reachable from a cold seed:
- conversations — harness (
claude,pi) × state (running,finished,idle) × lifecycle (nothing / committed+pushed / merged+deployed+notified), plus an archived one per harness, a subagent conversation linked to its parent, an empty transcript, and a kitchen-sink thread that contains everyThreadItemKind(text, thinking, tool_use, tool_result, interrupted, paused), every role, an error result, and one card per rendered tool: Read (+ image and screenshot widgets), Edit, Write (plan widget), Bash (+ a deploy URL), Grep, Glob, WebSearch, WebFetch, Skill, Task (subagent), TaskCreate. - files — every
FileEntryKind× editable × estimated. - services — every auth value (
auth,public,api,mixed, none) × router kind (http,tcp). - projects — with/without costs, url, package.json; both
OgImagekinds. - plans — every status (proposed / approved / in-progress / done), half with
an
actualrollup. memories — one per memory type. diffs — everyFileDiffStatusandDiffLineType, plus a binary and a truncated file.
It is deterministic (fixed clock, no Math.random), so a test can assert on ids,
counts and totals.
Test hooks
window.__mock.db() // the live database object
window.__mock.reset() // wipe localStorage + reseed
window.__mock.speed(0) // 0 = don't stream on a timer…
window.__mock.flush() // …land every in-flight run instantly (deterministic)
window.__mock.publish({type: "meta"}) // fire an SSE event by hand
A Puppeteer/Playwright pass therefore needs no fixtures of its own: load
/?mock=1, walk the routes, POST /api/spawn, flush(), assert.
Keeping it honest
The mock's rows are typed with the generated src/generated/model types
(re-exported by types.ts), so a backend schema change that lands in
openapi.json → npm run gen:api breaks the seed at tsc time instead of
silently drifting from the API it pretends to be.