#!/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 pluginContract } from "./scenarios/plugin-contract.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], ["plugin-contract", pluginContract], ["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());