lvmh e2e integration harness
Cross-component tests proving the seams between the three real components:
- daemon — the golang server, booted with
go run .from../daemon(real HTTP server, real sqlite store, real WS hubs) - web dist — the production
web/distbuild served BY the daemon via--webdist(built first if missing;npm installtoo whennode_modulesis absent) - plugin client — a scripted websocket client speaking exactly the
protocol the real
plugin/lvmh-agent.tsspeaks (PROTOCOL.mdv: 1, flattened envelopes,Authorization: Bearer …on the upgrade) — plus a fake GitLab v4 (/api/v4/user,/api/v4/projects) on an ephemeral port.
No Docker, no LLM key, no real GitLab needed. Zero npm dependencies
(Node built-ins only; the global WebSocket client, Node ≥ 22).
Run
make integration # or: make e2e-integration, or: bun e2e/driver.mjs
Prerequisites: go (toolchain for go run .) and bun (node >= 22 also works).
First run compiles the daemon and may build the web dist; later runs reuse
both. Exit code is non-zero when any check fails. LVMH_E2E_KEEP=1 keeps the
harness scratch dir (daemon log + db) under .pi/scratch/e2e-*/ for debugging.
What is covered
| scenario | proves |
|---|---|
auth |
REST 401 without/with wrong bearer; web /ws?token= rejected before upgrade. Agent-WS rejection is xfail: daemon Routes() serves /agent/ws without bearerAuth (protocol §Auth violation, real bug — see check reason). |
web-dist |
daemon serves the real built UI: GET / index, hashed /assets/*, manifest.webmanifest, SPA fallback. |
agent-lifecycle |
hello → welcome{lastSeq:0}, persisted events, GET /api/sessions online flag + snapshot fields, offline after disconnect, events?after= slicing, envelope re-serialization. |
replay |
reconnect welcome.lastSeq = persisted high-water; seq continuation; message_update fanned out live but never persisted; REST events unaffected. |
prompt-routing |
browser /ws subscribe → batched live events frames incl. message_update; POST /api/sessions/:id/prompt → prompt frame on the agent WS (promptId, envelope); 409 when offline. |
session-list |
session_list broadcasts to every web client on session change (second subscriber included), online flag flips on disconnect. |
gitlab |
POST /api/gitlab/connect against the fake GitLab (PAT forwarded as Private-Token), repos mapping + daemon-side sort by activity, status, PAT never present in any response, failed connect does not clobber the stored PAT. |
spawn-validation |
POST /api/spawn rejects non group/project repos and malformed JSON with 400; /api/spawn/status shape. (A valid spawn needs Docker and is intentionally out of scope.) |
resilience |
SIGKILL of the daemon process group + reboot on the same DB: sessions listed online:false, every persisted event intact, welcome.lastSeq honoured, seq continues past the high-water. |
Layout
driver.mjs boots daemon + fake gitlab, runs scenarios, summary, exit code
lib.mjs helpers: deadline polling, REST client, scripted WS clients
fake-gitlab.mjs minimal GitLab v4 on an ephemeral port
scenarios/*.mjs one module per scenario (assert-based checks)
Determinism: ephemeral ports everywhere, all waits are deadline-bounded polls
(waitUntil / frame waiters), never fixed sleeps. The daemon runs in its own
process group and is always killed on exit (including on crash or SIGINT).
xfail entries are known-broken assertions (documented real bugs): they do not
fail the run; if one starts passing it is reported as xpass so the xfail can
be dropped.