import { test, after } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync, chmodSync, statSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve, basename } from "node:path"; import { spawnSync, spawn } from "node:child_process"; import { createServer as createNetServer } from "node:net"; import { ConfigError, markSeen } from "../src/scan.mjs"; import { isLoopbackHost, startServer, DEFAULT_AGENT_SEND, REPLY_LIMIT, REPLY_TRAILER } from "../src/serve.mjs"; import { writeRegistration, makeRegistration } from "../../seat/src/seat.mjs"; const pkgRoot = resolve(import.meta.dirname, ".."); const cli = join(pkgRoot, "src", "cli.mjs"); // --------------------------------------------------------------------------- // Fixture helpers, copied from scan.test.mjs (kept local so that file stays // untouched; do not import unexported helpers across test files). // --------------------------------------------------------------------------- // Track every tmpdir we create so a stray failure never leaves fixtures behind. const roots = []; function makeRoot() { const root = mkdtempSync(join(tmpdir(), "control-board-serve-test-")); roots.push(root); return root; } after(() => { for (const root of roots) rmSync(root, { recursive: true, force: true }); }); function writeFile(path, content) { mkdirSync(resolve(path, ".."), { recursive: true }); writeFileSync(path, content); } function sessionLine({ id, timestamp, cwd }) { return JSON.stringify({ type: "session", id, timestamp, cwd }); } function messageLine({ timestamp, role, stopReason, texts }) { const message = { role }; if (stopReason !== undefined) message.stopReason = stopReason; if (texts) message.content = texts.map((text) => ({ type: "text", text })); return JSON.stringify({ type: "message", timestamp, message }); } function writeSessionFile(dir, name, lines, { trailingNewline = true } = {}) { const path = join(dir, name); writeFile(path, lines.join("\n") + (trailingNewline ? "\n" : "")); return path; } // --------------------------------------------------------------------------- // serve.mjs-specific helpers // --------------------------------------------------------------------------- function closeServer(server) { return new Promise((resolvePromise) => server.close(resolvePromise)); } // Grab an ephemeral free port, then hand it back immediately so a caller can // try to bind it themselves (used to prove startServer never opened a socket). function getFreePort() { return new Promise((resolvePromise, reject) => { const probe = createNetServer(); probe.once("error", reject); probe.listen(0, "127.0.0.1", () => { const port = probe.address().port; probe.close(() => resolvePromise(port)); }); }); } function runCli(args) { return spawnSync(process.execPath, [cli, ...args], { encoding: "utf8", timeout: 15000 }); } // --------------------------------------------------------------------------- // 1. isLoopbackHost // --------------------------------------------------------------------------- test("isLoopbackHost: recognizes loopback hosts", () => { assert.equal(isLoopbackHost("127.0.0.1"), true); assert.equal(isLoopbackHost("::1"), true); assert.equal(isLoopbackHost("localhost"), true); assert.equal(isLoopbackHost("127.5.5.5"), true); }); test("isLoopbackHost: rejects non-loopback hosts", () => { assert.equal(isLoopbackHost("0.0.0.0"), false); assert.equal(isLoopbackHost("192.168.1.2"), false); assert.equal(isLoopbackHost("::"), false); assert.equal(isLoopbackHost(""), false); assert.equal(isLoopbackHost("evil.example"), false); }); // --------------------------------------------------------------------------- // 2. startServer: fail-closed on a non-loopback host // --------------------------------------------------------------------------- test("startServer: refuses a non-loopback host with ConfigError, never opens a socket", async () => { const root = makeRoot(); const port = await getFreePort(); let caught = null; try { // startServer is async, so the host refusal surfaces as a rejection // before any listen() call happens. await startServer({ host: "192.168.1.2", port, specs: [], boardDir: join(root, "board"), isAlive: () => true, }); } catch (err) { caught = err; } assert.ok(caught instanceof ConfigError, "expected a ConfigError"); // The port must still be free: startServer must never have called listen(). await new Promise((resolvePromise, reject) => { const probe = createNetServer(); probe.once("error", reject); probe.listen(port, "127.0.0.1", () => probe.close(resolvePromise)); }); }); // --------------------------------------------------------------------------- // 3. createServer routes, via startServer on port 0 // --------------------------------------------------------------------------- test("startServer: serves page, healthz, and a rescanning /api/board", async () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done"] }), ]); const boardDir = join(root, "board"); const page = "injected test page"; const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }]; const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir, isAlive: () => true, page, }); const base = `http://127.0.0.1:${server.address().port}`; try { for (const path of ["/", "/index.html"]) { const res = await fetch(`${base}${path}`); assert.equal(res.status, 200, path); assert.equal(res.headers.get("content-type"), "text/html; charset=utf-8", path); assert.equal(res.headers.get("cache-control"), "no-store", path); assert.equal(await res.text(), page, path); } const health = await fetch(`${base}/healthz`); assert.equal(health.status, 200); assert.deepEqual(await health.json(), { ok: true }); const first = await fetch(`${base}/api/board`); assert.equal(first.status, 200); assert.equal(first.headers.get("content-type"), "application/json"); assert.equal(first.headers.get("cache-control"), "no-store"); const firstBody = await first.json(); assert.equal(firstBody.sessions[0].state, "waiting"); assert.ok(firstBody.waitingOnYou.includes("proj/agent1")); assert.ok(existsSync(join(boardDir, "index.json")), "index.json must be written by the scan"); assert.ok(existsSync(join(boardDir, "sessions", "proj", "agent1.json")), "per-agent file must be written by the scan"); // Rewrite the fixture to a "user" last message (working state) and hit // /api/board again: a fresh scan must reflect the new state, proving // each request rescans instead of caching. writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:05:00Z", role: "user", texts: ["go again"] }), ]); const second = await fetch(`${base}/api/board`); assert.equal(second.status, 200); const secondBody = await second.json(); assert.equal(secondBody.sessions[0].state, "working"); const missing = await fetch(`${base}/nope`); assert.equal(missing.status, 404); const posted = await fetch(`${base}/api/board`, { method: "POST" }); assert.equal(posted.status, 405); } finally { await closeServer(server); } }); test("startServer: a seatsDir registration overrides the row and index.registered reflects it", async () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "user", texts: ["derived task"] }), ]); const boardDir = join(root, "board"); const seatsDir = join(root, "seats"); const reg = makeRegistration({ resolved: { seat: "agent1", project: "proj", sessionsDir, seatDir: join(root, "agent1"), launchScript: join(root, "agent1", "launch.sh"), layout: "repo", defaultWorkspace: null, }, task: "registered task", }); writeRegistration(seatsDir, reg); const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }]; const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir, seatsDir, isAlive: () => ({ alive: true, workspace: null }), page: "", }); const base = `http://127.0.0.1:${server.address().port}`; try { const res = await fetch(`${base}/api/board`); assert.equal(res.status, 200); const body = await res.json(); assert.equal(body.sessions[0].taskSource, "registration"); assert.equal(body.sessions[0].task, "registered task"); assert.deepEqual(body.registered, ["proj/agent1"]); assert.deepEqual(body.registrationErrors, []); } finally { await closeServer(server); } }); // --------------------------------------------------------------------------- // 4. /api/board: scan failure surfaces as a 500 with an error field // --------------------------------------------------------------------------- test("startServer: /api/board returns 500 JSON with an error field when scan throws", async () => { const server = await startServer({ host: "127.0.0.1", port: 0, specs: [], // Relative boardDir: scan() throws ConfigError("boardDir must be an absolute path"). boardDir: "relative/board", isAlive: () => true, page: "", }); const base = `http://127.0.0.1:${server.address().port}`; try { const res = await fetch(`${base}/api/board`); assert.equal(res.status, 500); assert.equal(res.headers.get("content-type"), "application/json"); const body = await res.json(); assert.equal(typeof body.error, "string"); assert.ok(body.error.length > 0); } finally { await closeServer(server); } }); // --------------------------------------------------------------------------- // 5. CLI // --------------------------------------------------------------------------- test("CLI: serve refuses a non-loopback host with exit 2 and a refused: message", () => { const root = makeRoot(); const dataRoot = join(root, "data"); const configPath = join(root, "config.json"); writeFile(configPath, JSON.stringify({ dataRoot })); const repoRoot = join(root, "repo"); mkdirSync(repoRoot, { recursive: true }); const r = runCli(["serve", "--host", "0.0.0.0", "--config", configPath, "--repo", repoRoot, "--fleet", "none"]); assert.equal(r.status, 2); assert.match(r.stderr, /^refused:/); }); test("CLI: serve rejects a non-numeric --port with exit 2", () => { const r = runCli(["serve", "--port", "abc"]); assert.equal(r.status, 2); assert.match(r.stderr, /^refused:/); }); test("CLI: scan still works after the async cli refactor", () => { const root = makeRoot(); const dataRoot = join(root, "data"); const configPath = join(root, "config.json"); writeFile(configPath, JSON.stringify({ dataRoot })); const repoRoot = join(root, "repo"); mkdirSync(repoRoot, { recursive: true }); const r = runCli(["scan", "--config", configPath, "--repo", repoRoot, "--fleet", "none", "--liveness", "assume-alive"]); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /^board: /m); }); test("CLI: live serve prints its URL and answers /healthz", async () => { const root = makeRoot(); const dataRoot = join(root, "data"); const configPath = join(root, "config.json"); writeFile(configPath, JSON.stringify({ dataRoot })); const repoRoot = join(root, "repo"); mkdirSync(repoRoot, { recursive: true }); const child = spawn( process.execPath, [cli, "serve", "--port", "0", "--liveness", "assume-alive", "--config", configPath, "--repo", repoRoot, "--fleet", "none"], { stdio: ["ignore", "pipe", "pipe"] } ); let stdoutBuf = ""; let stderrBuf = ""; child.stderr.on("data", (chunk) => { stderrBuf += chunk.toString(); }); let url; try { url = await new Promise((resolvePromise, reject) => { const timer = setTimeout(() => { reject(new Error(`timed out waiting for the server line; stdout=${JSON.stringify(stdoutBuf)} stderr=${JSON.stringify(stderrBuf)}`)); }, 15000); child.stdout.on("data", (chunk) => { stdoutBuf += chunk.toString(); const match = stdoutBuf.match(/^control board: (http:\/\/127\.0\.0\.1:\d+)\//m); if (match) { clearTimeout(timer); resolvePromise(match[1]); } }); child.on("exit", (code) => { clearTimeout(timer); reject(new Error(`child exited early with code ${code}; stderr=${stderrBuf}`)); }); }); const res = await fetch(`${url}/healthz`); assert.equal(res.status, 200); assert.deepEqual(await res.json(), { ok: true }); } finally { child.kill("SIGTERM"); await new Promise((resolvePromise) => { if (child.exitCode !== null || child.signalCode !== null) return resolvePromise(); child.on("exit", resolvePromise); }); } }); // The page's only XSS defence is its inline esc() helper. Pull that function // out of page.html by name and check it inerts every HTML-significant char. test("page.html: esc() escapes every HTML-significant character", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const m = html.match(/function esc\(v\) \{[\s\S]*?\n \}/); assert.ok(m, "esc() must exist in page.html"); const esc = new Function(`${m[0]}; return esc;`)(); assert.equal(esc('&\''), "<script>alert("x")</script>&'"); assert.equal(esc(null), ""); assert.equal(esc(undefined), ""); assert.equal(esc(42), "42"); // The page builds HTML by string concatenation. Any API value joined // straight into markup ("+ rec.x" / "+ project" / "+ data.x") would bypass // esc(); require zero such joins so a regression is caught here. const rawJoins = [...html.matchAll(/\+\s*(rec\.[\w.]+|project|data\.[\w.]+)\b(?!\s*\|\|)/g)].map((x) => x[0]); assert.deepEqual(rawJoins, [], `API values concatenated into HTML without esc(): ${rawJoins.join(" | ")}`); const escCalls = (html.match(/\besc\(/g) || []).length; assert.ok(escCalls >= 15, `expected many esc() calls, saw ${escCalls}`); }); // --------------------------------------------------------------------------- // 6. POST /api/seen // --------------------------------------------------------------------------- test("POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it", async () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done"] }), ]); const boardDir = join(root, "board"); const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }]; const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; try { const postRes = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "proj", agent: "agent1", lastActivity: "2026-09-01T00:00:01Z" }), }); assert.equal(postRes.status, 200); const postBody = await postRes.json(); assert.ok(postBody.seen.includes("proj/agent1")); assert.ok(!postBody.waitingOnYou.includes("proj/agent1")); const boardRes = await fetch(`${base}/api/board`); assert.equal(boardRes.status, 200); const boardBody = await boardRes.json(); assert.ok(boardBody.seen.includes("proj/agent1")); assert.ok(!boardBody.waitingOnYou.includes("proj/agent1")); const clearRes = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "proj", agent: "agent1", lastActivity: "2026-09-01T00:00:01Z", seen: false }), }); assert.equal(clearRes.status, 200); const clearBody = await clearRes.json(); assert.ok(!clearBody.seen.includes("proj/agent1")); assert.ok(clearBody.waitingOnYou.includes("proj/agent1")); } finally { await closeServer(server); } }); test("POST /api/seen without a JSON content-type returns 400 and does not write a mark", async () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done"] }), ]); const boardDir = join(root, "board"); const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }]; const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; try { const res = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "text/plain" }, body: JSON.stringify({ project: "proj", agent: "agent1", lastActivity: "2026-09-01T00:00:01Z" }), }); assert.equal(res.status, 400); assert.ok(!existsSync(join(boardDir, "seen.json")), "seen.json must not be written on a rejected content-type"); } finally { await closeServer(server); } }); test("POST /api/seen with invalid JSON returns 400", async () => { const root = makeRoot(); const boardDir = join(root, "board"); const server = await startServer({ host: "127.0.0.1", port: 0, specs: [], boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; try { const res = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "application/json" }, body: "{ not json", }); assert.equal(res.status, 400); assert.ok(!existsSync(join(boardDir, "seen.json"))); } finally { await closeServer(server); } }); test("POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark", async () => { const root = makeRoot(); const boardDir = join(root, "board"); const server = await startServer({ host: "127.0.0.1", port: 0, specs: [], boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; const big = JSON.stringify({ project: "proj", agent: "agent1", lastActivity: "x".repeat(5000) }); try { let status = null; try { const res = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "application/json" }, body: big, }); status = res.status; } catch { status = null; // a destroyed connection is an acceptable outcome too } if (status !== null) assert.equal(status, 400); assert.ok(!existsSync(join(boardDir, "seen.json")), "seen.json must not be written for an oversized body"); } finally { await closeServer(server); } }); test("POST /api/seen with a missing agent returns 400", async () => { const root = makeRoot(); const boardDir = join(root, "board"); const server = await startServer({ host: "127.0.0.1", port: 0, specs: [], boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; try { const res = await fetch(`${base}/api/seen`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "proj", lastActivity: "2026-09-01T00:00:01Z" }), }); assert.equal(res.status, 400); assert.ok(!existsSync(join(boardDir, "seen.json"))); } finally { await closeServer(server); } }); test("POST /api/board returns 405; PUT /api/seen returns 405", async () => { const root = makeRoot(); const boardDir = join(root, "board"); const server = await startServer({ host: "127.0.0.1", port: 0, specs: [], boardDir, isAlive: () => true, page: "" }); const base = `http://127.0.0.1:${server.address().port}`; try { const board = await fetch(`${base}/api/board`, { method: "POST" }); assert.equal(board.status, 405); const put = await fetch(`${base}/api/seen`, { method: "PUT" }); assert.equal(put.status, 405); } finally { await closeServer(server); } }); // --------------------------------------------------------------------------- // 7. CLI: seen rows in --print // --------------------------------------------------------------------------- test("CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)'", () => { const root = makeRoot(); const dataRoot = join(root, "data"); const configPath = join(root, "config.json"); writeFile(configPath, JSON.stringify({ dataRoot })); const repoRoot = join(root, "repo"); const sessionsDir = join(repoRoot, ".pi", "state", "agent1", "sessions"); mkdirSync(sessionsDir, { recursive: true }); const lastActivity = "2026-09-01T00:00:01Z"; writeFile( join(sessionsDir, "s.jsonl"), [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: lastActivity, role: "assistant", stopReason: "stop", texts: ["done"] }), ].join("\n") + "\n" ); const boardDir = join(dataRoot, "board"); const project = basename(resolve(repoRoot)); markSeen(boardDir, { project, agent: "agent1", lastActivity, seen: true }); const r = runCli(["scan", "--config", configPath, "--repo", repoRoot, "--fleet", "none", "--liveness", "assume-alive", "--print"]); assert.equal(r.status, 0, r.stderr); const row = r.stdout.split("\n").find((l) => l.includes("agent1")); assert.ok(row, `expected an agent1 row in:\n${r.stdout}`); assert.equal(row[0], "s"); assert.match(r.stdout, /1 seen, 0 registered, 0 stale\)\s*$/m); }); // --------------------------------------------------------------------------- // 8. page.html: the seen-toggle control escapes its values and posts JSON // --------------------------------------------------------------------------- test("page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const m = html.match(/function seenControl\(rec\) \{[\s\S]*?\n \}/); assert.ok(m, "seenControl() must exist in page.html"); const body = m[0]; assert.match(body, /esc\(rec\.project\)/); assert.match(body, /esc\(rec\.agent\)/); assert.match(body, /esc\(rec\.lastActivity\)/); assert.match(html, /headers:\s*\{\s*"content-type":\s*"application\/json"\s*\}/, "the seen POST must send content-type: application/json"); }); test("page.html: has a collapsed Seen section that lists seen rows with the shared row builder", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); assert.match(html, /
(?![^>]*\sopen)/, "the Seen section must start collapsed"); assert.match(html, /

Seen<\/h2>/); const m = html.match(/function renderSeen\(data\) \{[\s\S]*?\n \}/); assert.ok(m, "renderSeen() must exist in page.html"); assert.match(m[0], /filter\(function \(r\) \{ return r\.seen; \}\)/, "the Seen section lists exactly the rows the API marks seen"); assert.match(m[0], /buildRowPair\(r, true\)/, "seen rows reuse the escaped row builder, project column included"); assert.match(html, /renderWaiting\(lastData\);\n\s*renderSeen\(lastData\);/, "renderAll must render the Seen section on every refresh"); }); test("page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const m = html.match(/function renderProjects\(data\) \{[\s\S]*?\n \}/); assert.ok(m, "renderProjects() must exist in page.html"); const body = m[0]; assert.match(body, /if \(!\(project in hideSeenState\)\) hideSeenState\[project\] = true/, "Hide seen defaults to on and the choice survives re-render"); assert.match(body, /class="hide-seen-toggle" data-project="' \+ esc\(project\)/, "the checkbox carries the escaped project name"); assert.match(body, /hideSeen && r\.seen/, "seen rows are filtered when the box is ticked"); assert.match(body, /" seen hidden"/, "the note reports how many seen rows are hidden"); assert.match(html, /closest\("\.hide-offline-toggle, \.hide-seen-toggle"\)/, "one change handler serves both checkboxes"); }); test("page.html: a project header reads \"N of N\" only while a checkbox hides rows", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const m = html.match(/function renderProjects\(data\) \{[\s\S]*?\n \}/); assert.ok(m, "renderProjects() must exist in page.html"); const body = m[0]; assert.match( body, /var headCount = visible\.length < group\.length \? visible\.length \+ " of " \+ group\.length : String\(group\.length\)/, "shown-of-total only when something is hidden; the plain total otherwise", ); assert.match(body, /\(' \+ headCount \+ "\)<\/span>/, "the header uses headCount, not the raw group length"); assert.doesNotMatch(body, /\(' \+ group\.length/, "the raw group length must no longer be rendered in the header"); }); test("page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const m = html.match(/function buildRowPair\(rec, showProject\) \{[\s\S]*?\n \}/); assert.ok(m, "buildRowPair() must exist in page.html"); const body = m[0]; assert.match(body, /rec\.task\s*\?[\s\S]*esc\(rec\.task\)[\s\S]*:\s*'unknown<\/span>'/, "task is escaped, or reads unknown when null"); assert.match(body, /rec\.activeProject\s*\?[\s\S]*esc\(rec\.activeProject\)[\s\S]*unknown/, "active project is escaped, or reads unknown when null"); assert.match(body, /
Task<\/dt>/, "detail lists the task"); assert.match(body, /
Active project<\/dt>/, "detail lists the active project"); assert.match(body, /
Workspace<\/dt>
" \+ \(rec\.workspace \? esc\(rec\.workspace\) : "unknown"\)/, "detail lists the workspace, escaped, or unknown"); assert.match(body, /var span = showProject \? 7 : 6/, "the detail row spans the two new columns"); const heads = html.match(/Task<\/th>Active project<\/th>/g) || []; assert.equal(heads.length, 3, "all three tables carry the two new headers"); assert.match(html, /No agents\.<\/td>/, "the empty project row spans every column"); }); // --------------------------------------------------------------------------- // 9. page.html: registration source tags and the Registered detail row // --------------------------------------------------------------------------- test("page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const rowPair = html.match(/function buildRowPair\(rec, showProject\) \{[\s\S]*?\n \}/); assert.ok(rowPair, "buildRowPair() must exist in page.html"); assert.match(rowPair[0], /sourceTag\(rec\.taskSource\)/, "the task cell shows its source"); assert.match(rowPair[0], /sourceTag\(rec\.activeProjectSource\)/, "the active project cell shows its source"); assert.match( rowPair[0], /
Registered<\/dt>
" \+ registeredText\(rec\.registered\) \+ "<\/dd>/, "the detail list has a Registered row built by registeredText()", ); assert.match(rowPair[0], /esc\(rec\.agent\) \+ "<\/button>" \+ modelTag\(rec\)/, "the agent cell shows the model under the name"); assert.match(rowPair[0], /
Model<\/dt>
" \+ modelText\(rec\) \+ "<\/dd>/, "the detail list has a Model row"); for (const name of ["modelTag", "modelText"]) { const line = html.split("\n").find((l) => l.includes(`function ${name}(rec)`)); assert.ok(line, `${name}() must exist in page.html`); assert.match(line, /esc\(rec\.model\)/, `${name}() escapes the model`); if (name === "modelText") assert.match(line, /esc\(rec\.provider\)/, "modelText() escapes the provider"); } const sourceLabelMatch = html.match(/var SOURCE_LABEL = (\{.*\});/); assert.ok(sourceLabelMatch, "SOURCE_LABEL must exist in page.html"); const sourceLabel = JSON.parse(sourceLabelMatch[1]); assert.equal(sourceLabel.registration, "registered", 'SOURCE_LABEL must map "registration" to "registered"'); // sourceTag() and fromSource() are one-liners in page.html; match them by // line rather than by a "function ... { ... \n }" block regex, which // assumes a closing brace on its own line. const lines = html.split("\n"); const sourceTagLine = lines.find((l) => l.includes("function sourceTag(source)")); assert.ok(sourceTagLine, "sourceTag() must exist in page.html"); assert.match(sourceTagLine, /esc\(source\)/, "sourceTag() escapes the raw source value (used in the title)"); assert.match(sourceTagLine, /esc\(sourceLabel\(source\)\)/, "sourceTag() escapes the label text it displays"); const fromSourceLine = lines.find((l) => l.includes("function fromSource(source)")); assert.ok(fromSourceLine, "fromSource() must exist in page.html"); assert.match(fromSourceLine, /esc\(sourceLabel\(source\)\)/, "fromSource() escapes the label text it displays"); const registeredTextFn = html.match(/function registeredText\(reg\) \{[\s\S]*?\n \}/); assert.ok(registeredTextFn, "registeredText() must exist in page.html"); for (const line of registeredTextFn[0].split("\n")) { if (!/\breg\.[a-zA-Z]/.test(line)) continue; assert.match(line, /esc\(/, `every dynamic value read off reg must be escaped: ${line.trim()}`); } }); // --------------------------------------------------------------------------- // 10. POST /api/reply: reply-from-board through a fake agent-send.sh (#1505) // --------------------------------------------------------------------------- // A fake tools/tmux/agent-send.sh: records argv and the env it saw, exits // with FAKE_SEND_EXIT (default 0) and writes FAKE_SEND_STDERR to stderr. function writeFakeAgentSend(root) { const path = join(root, "agent-send.sh"); writeFile(path, `#!/usr/bin/env node const fs = require("node:fs"); fs.writeFileSync(process.env.FAKE_SEND_CAPTURE, JSON.stringify({ argv: process.argv.slice(2), tmuxSocketEnv: process.env.MOSAIC_TMUX_SOCKET ?? null })); process.stdout.write("fake sent\\n"); if (process.env.FAKE_SEND_STDERR) process.stderr.write(process.env.FAKE_SEND_STDERR); process.exit(Number(process.env.FAKE_SEND_EXIT || 0)); `); chmodSync(path, 0o755); return path; } function replyFixture({ tmux = { socket: null, session: "agent1" }, pid = process.pid, registered = true } = {}) { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done?"] }), ]); const seatsDir = join(root, "seats"); if (registered) { writeRegistration(seatsDir, makeRegistration({ resolved: { seat: "agent1", project: "proj", sessionsDir, seatDir: join(root, "agent1"), launchScript: join(root, "agent1", "launch.sh"), layout: "repo", defaultWorkspace: null }, task: "t", tmux, pid, })); } const capture = join(root, "capture.json"); process.env.FAKE_SEND_CAPTURE = capture; return { root, sessionsDir, seatsDir, boardDir: join(root, "board"), agentSend: writeFakeAgentSend(root), capture, specs: [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }] }; } async function postReply(base, body, headers = { "content-type": "application/json" }) { const res = await fetch(`${base}/api/reply`, { method: "POST", headers, body: typeof body === "string" ? body : JSON.stringify(body) }); return { status: res.status, body: await res.json() }; } test("POST /api/reply: runs agent-send.sh with -s from the registration, -S :control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams", async () => { const f = replyFixture(); const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "", agentSend: f.agentSend, now: () => new Date("2026-09-12T18:00:00Z") }); const base = `http://127.0.0.1:${server.address().port}`; process.env.MOSAIC_TMUX_SOCKET = "leaked-from-launcher"; delete process.env.FAKE_SEND_EXIT; delete process.env.FAKE_SEND_STDERR; try { const { status, body } = await postReply(base, { agent: "proj/agent1", text: "yes, go ahead" }); assert.equal(status, 200); assert.equal(body.delivered, true); assert.equal(body.exitCode, 0); assert.equal(body.stdout, "fake sent\n"); assert.equal(body.stderr, ""); assert.equal(body.session, "agent1"); assert.equal(body.socket, null); assert.equal(body.sentAt, "2026-09-12T18:00:00.000Z"); const seen = JSON.parse(readFileSync(f.capture, "utf8")); assert.equal(seen.tmuxSocketEnv, null, "the launcher's socket variable must not reach the tool"); const argv = seen.argv; assert.equal(argv[argv.indexOf("-s") + 1], "agent1"); assert.match(argv[argv.indexOf("-S") + 1], /^[^:\s]+:control-board$/); assert.equal(argv[argv.indexOf("-m") + 1], "yes, go ahead\n" + REPLY_TRAILER, "the text, then the fixed trailer on its own line"); assert.match(REPLY_TRAILER, /^\(control-board: .*Do not agent-send to control-board\.\)$/); assert.equal(argv.includes("-L"), false); assert.equal(argv.length, 6, "exactly -s, -S and -m: no send-keys, class or retries"); } finally { delete process.env.MOSAIC_TMUX_SOCKET; await closeServer(server); } }); test("POST /api/reply: a registration with a tmux socket adds -L ", async () => { const f = replyFixture({ tmux: { socket: "mosaic-fleet", session: "agent1" } }); const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "", agentSend: f.agentSend }); const base = `http://127.0.0.1:${server.address().port}`; try { const { status, body } = await postReply(base, { agent: "proj/agent1", text: "hi" }); assert.equal(status, 200); assert.equal(body.socket, "mosaic-fleet"); const argv = JSON.parse(readFileSync(f.capture, "utf8")).argv; assert.equal(argv[argv.indexOf("-L") + 1], "mosaic-fleet"); } finally { await closeServer(server); } }); test("POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim", async () => { const f = replyFixture(); const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "", agentSend: f.agentSend }); const base = `http://127.0.0.1:${server.address().port}`; process.env.FAKE_SEND_EXIT = "4"; process.env.FAKE_SEND_STDERR = "agent-send.sh: REFUSING - session 'agent1' exists on multiple sockets\n"; try { const { status, body } = await postReply(base, { agent: "proj/agent1", text: "hi" }); assert.equal(status, 200); assert.equal(body.delivered, false); assert.equal(body.exitCode, 4); assert.match(body.stderr, /REFUSING/); } finally { delete process.env.FAKE_SEND_EXIT; delete process.env.FAKE_SEND_STDERR; await closeServer(server); } }); test("POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called", async () => { const cases = [ { fixture: replyFixture(), body: { agent: "proj/agent1", text: "" }, status: 400, error: /non-empty/ }, { fixture: replyFixture(), body: { agent: "proj/agent1", text: " \n" }, status: 400, error: /non-empty/ }, { fixture: replyFixture(), body: { agent: "proj/agent1", text: "x".repeat(REPLY_LIMIT + 1) }, status: 400, error: /longer than/ }, { fixture: replyFixture(), body: { agent: "proj/nobody", text: "hi" }, status: 404, error: /unknown row/ }, { fixture: replyFixture(), body: { text: "hi" }, status: 400, error: /row id/ }, { fixture: replyFixture({ registered: false }), body: { agent: "proj/agent1", text: "hi" }, status: 409, error: /needs a registered seat/ }, { fixture: replyFixture({ pid: 4242 }), isPidAlive: () => false, body: { agent: "proj/agent1", text: "hi" }, status: 409, error: /stale/ }, { fixture: replyFixture({ tmux: null }), body: { agent: "proj/agent1", text: "hi" }, status: 409, error: /no tmux session/ }, { fixture: replyFixture(), body: "{not json", status: 400, error: /invalid JSON/ }, ]; for (const c of cases) { const f = c.fixture; const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "", agentSend: f.agentSend, isPidAlive: c.isPidAlive }); const base = `http://127.0.0.1:${server.address().port}`; try { const { status, body } = await postReply(base, c.body); assert.equal(status, c.status, `${JSON.stringify(c.body).slice(0, 60)}: ${JSON.stringify(body)}`); assert.match(body.error, c.error); assert.equal(existsSync(f.capture), false, "the tool must not run on a refusal"); } finally { await closeServer(server); } } }); test("POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash", async () => { const f = replyFixture(); const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "", agentSend: join(f.root, "missing-agent-send.sh") }); const base = `http://127.0.0.1:${server.address().port}`; try { const { status, body } = await postReply(base, { agent: "proj/agent1", text: "hi" }); assert.equal(status, 500); assert.match(body.error, /missing-agent-send\.sh/); const health = await fetch(`${base}/healthz`); assert.equal(health.status, 200); } finally { await closeServer(server); } }); test("replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable", () => { assert.equal(DEFAULT_AGENT_SEND, resolve(pkgRoot, "..", "..", "tools", "tmux", "agent-send.sh")); assert.ok(existsSync(DEFAULT_AGENT_SEND)); assert.ok(statSync(DEFAULT_AGENT_SEND).mode & 0o111); }); test("page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped", () => { const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8"); const lines = html.split("\n"); const canReplyFn = html.match(/function canReply\(rec\) \{[\s\S]*?\n \}/); assert.ok(canReplyFn, "canReply() must exist"); assert.match(canReplyFn[0], /reg\.alive !== false/, "a stale registration gets no box"); assert.match(canReplyFn[0], /reg\.tmux && reg\.tmux\.session/, "a registration without a tmux session gets no box"); const replyControlFn = html.match(/function replyControl\(rec\) \{[\s\S]*?\n \}/); assert.ok(replyControlFn, "replyControl() must exist"); assert.match(replyControlFn[0], /if \(!canReply\(rec\)\) return 'reply needs a registered seat<\/span>'/); assert.match(replyControlFn[0], /class="reply-form" data-id="' \+ esc\(id\)/); assert.match(replyControlFn[0], /value="' \+ esc\(replyDrafts\[id\] \|\| ""\)/, "an unsent draft is put back after a refresh"); const rowPair = html.match(/function buildRowPair\(rec, showProject\) \{[\s\S]*?\n \}/); assert.match(rowPair[0], /
Reply<\/dt>
" \+ replyControl\(rec\) \+ "<\/dd>/); const receiptFn = html.match(/function replyReceipt\(id\) \{[\s\S]*?\n \}/); assert.ok(receiptFn, "replyReceipt() must exist"); for (const line of receiptFn[0].split("\n")) { if (!/\br\.[a-zA-Z]/.test(line)) continue; assert.match(line, /esc\(/, `every receipt value must be escaped: ${line.trim()}`); } assert.match(receiptFn[0], /esc\(r\.stderr \|\| r\.error/, "a failure shows the tool's stderr"); const postReplyFn = html.match(/function postReply\(form\) \{[\s\S]*?\n \}/); assert.ok(postReplyFn, "postReply() must exist"); assert.match(postReplyFn[0], /fetch\("\/api\/reply", \{\s*method: "POST",\s*headers: \{ "content-type": "application\/json" \}/); assert.match(postReplyFn[0], /JSON\.stringify\(\{ agent: id, text: text \}\)/); assert.match(postReplyFn[0], /replyReceipts\[id\] = body/); assert.ok(lines.some((l) => l.includes('main.addEventListener("submit"')), "the form submits through one delegated handler"); assert.ok(lines.some((l) => l.includes('main.addEventListener("input"')), "drafts are captured as they are typed"); assert.match(html, /function withReplyFocus\(render\)/, "a refresh gives the caret back to the reply box"); assert.doesNotMatch(html, /send-keys/, "the page never talks tmux"); });