e2e: integration harness (69/69 green) + fix /agent/ws missing bearer auth (BUG-1)

This commit is contained in:
Raphael Westphal
2026-08-18 14:13:15 +02:00
parent 6ba718730e
commit ecb91e941c
17 changed files with 1129 additions and 2 deletions
+58
View File
@@ -0,0 +1,58 @@
# 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.