# 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/dist` build served BY the daemon via `--webdist` (built first if missing; `npm install` too when `node_modules` is absent) - **plugin client** — a scripted websocket client speaking exactly the protocol the real `plugin/lvmh-agent.ts` speaks (PROTOCOL.md `v: 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 ```sh make integration # or: make e2e-integration, or: node e2e/driver.mjs ``` Prerequisites: `go` (toolchain for `go run .`) and `node >= 22`. 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.