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
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env node
// driver.mjs — lvmh cross-integration harness.
//
// Proves the seams between the three REAL components:
// • daemon — the golang binary, booted via `go run .` from ../daemon
// • web — the built web/dist served BY the daemon (--webdist)
// • plugin — a scripted WS client speaking exactly the protocol the real
// plugin speaks (PROTOCOL.md v1, flattened envelopes, bearer
// header on upgrade — see plugin/lvmh-agent.ts)
//
// Plus a fake GitLab v4 on an ephemeral port (GITLAB_BASE_URL).
// Deterministic: ephemeral ports, deadline-bounded polling, no fixed sleeps.
// Zero npm dependencies (Node >= 22).
//
// Exit code: 0 green, 1 any failed check. Daemon process group is always
// killed on exit (normal, crash, SIGINT/SIGTERM).
import * as fs from "node:fs";
import * as path from "node:path";
import { startFakeGitLab } from "./fake-gitlab.mjs";
import {
DAEMON_DIR,
REPO_ROOT,
Results,
TEST_TOKEN,
WEB_DIST,
ensureWebDist,
freePort,
startDaemon,
} from "./lib.mjs";
import { run as auth } from "./scenarios/auth.mjs";
import { run as webDist } from "./scenarios/web-dist.mjs";
import { run as agentLifecycle } from "./scenarios/agent-lifecycle.mjs";
import { run as replay } from "./scenarios/replay.mjs";
import { run as promptRouting } from "./scenarios/prompt-routing.mjs";
import { run as sessionList } from "./scenarios/session-list.mjs";
import { run as gitlab } from "./scenarios/gitlab.mjs";
import { run as spawnValidation } from "./scenarios/spawn-validation.mjs";
import { run as resilience } from "./scenarios/resilience.mjs";
const SCENARIOS = [
["auth", auth],
["web-dist", webDist],
["agent-lifecycle", agentLifecycle],
["replay", replay],
["prompt-routing", promptRouting],
["session-list", sessionList],
["gitlab", gitlab],
["spawn-validation", spawnValidation],
["resilience", resilience], // last: SIGKILLs and reboots the daemon
];
const r = new Results();
let daemon = null;
let fakeGitLab = null;
let tmpDir = null;
let cleanedUp = false;
async function cleanup() {
if (cleanedUp) return;
cleanedUp = true;
if (daemon) await daemon.stop("SIGKILL");
if (fakeGitLab) fakeGitLab.close();
if (tmpDir && process.env.LVMH_E2E_KEEP !== "1") {
fs.rmSync(tmpDir, { recursive: true, force: true });
} else if (tmpDir) {
console.log(`(kept harness artifacts: ${tmpDir})`);
}
}
process.on("SIGINT", () => {
void cleanup().finally(() => process.exit(130));
});
process.on("SIGTERM", () => {
void cleanup().finally(() => process.exit(143));
});
async function main() {
console.log("=== lvmh e2e integration ===");
const built = ensureWebDist();
console.log(`web dist: ${built ? "built now" : "reused"} ${path.relative(REPO_ROOT, WEB_DIST)}`);
fs.mkdirSync(path.join(REPO_ROOT, ".pi", "scratch"), { recursive: true });
tmpDir = fs.mkdtempSync(path.join(REPO_ROOT, ".pi", "scratch", "e2e-"));
const dbPath = path.join(tmpDir, "lvmh.db");
const logPath = path.join(tmpDir, "daemon.log");
fakeGitLab = await startFakeGitLab();
const port = await freePort();
const addr = `127.0.0.1:${port}`;
const daemonEnv = {
LVMH_TOKEN: TEST_TOKEN,
LVMH_DB: dbPath,
GITLAB_BASE_URL: fakeGitLab.url,
LVMH_REPO_DIR: path.join(tmpDir, "repos"),
LVMH_CONTAINER_LVMH_URL: `ws://${addr}/agent/ws`,
LVMH_WORKER_DOCKERFILE: path.join(tmpDir, "absent-worker.Dockerfile"),
};
const boot = () => startDaemon({ addr, dbPath, webdist: WEB_DIST, logPath, env: daemonEnv });
daemon = await boot();
console.log(`daemon: ${daemon.baseUrl} (go run . in ${path.relative(REPO_ROOT, DAEMON_DIR)}, db ${dbPath})`);
console.log(`fake gitlab: ${fakeGitLab.url}`);
const ctx = {
r,
state: {},
base: daemon.baseUrl,
token: TEST_TOKEN,
agentUrl: daemon.agentUrl,
webUrl: daemon.webUrl,
gitlab: fakeGitLab,
restartDaemon: async () => {
await daemon.stop("SIGKILL");
daemon = await boot();
},
};
for (const [name, scenario] of SCENARIOS) {
r.group(name);
try {
await scenario(ctx);
} catch (err) {
r.check(`${name}: scenario crashed`, false, String(err?.stack ?? err));
const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8").split("\n").slice(-15).join("\n") : "";
if (log) console.log(` daemon.log tail:\n${log.split("\n").map((l) => " " + l).join("\n")}`);
}
}
const { ok, fail, xfail, xpass, total } = r.summary();
console.log("\n──────────────────────────────");
console.log(`passed ${ok} / ${total} failed ${fail} xfail ${xfail} (known bugs) xpass ${xpass}`);
if (fail > 0) {
console.log("failed checks:");
for (const e of r.entries.filter((e) => e.status === "fail")) {
console.log(` [${e.scenario}] ${e.name}${e.detail ? " — " + e.detail : ""}`);
}
}
console.log(`RESULT: ${fail === 0 ? "GREEN" : "RED"}`);
process.exitCode = fail === 0 ? 0 : 1;
}
main()
.catch((err) => {
console.error("harness crashed:", err);
process.exitCode = 1;
})
.finally(() => cleanup());