Files
stack/packages/control-board/tests/serve.test.mjs
T
jason.woltjeandClaude Opus 5.5 a5beb6d97d feat(conversation): CHAT-02 read-only Pi history reader and two board routes (#1507)
packages/conversation is a library with no server: safe-fs, the Pi session
parser, CHAT-01 pages, pinned snapshots, cursors and follow. The control
board adds GET /api/conversations and /api/conversation behind the Host
and Origin guard. Both are read-only, their queries are validated, and
each refusal code maps to a status.

Dewey authored it (packet 0cf177b1, revision 2). Filbert reviewed the code:
R1 revise (branch ids moving on append, the assumed-link bridge merging
branches, one unreadable seat directory turning the catalogue into a 500),
then R2 approve (3b14d66c). Darkwing reviewed the routes: R1 approve
(07b10ad1), R2 approve (b9d92003). The package lands with the routes,
because serve.mjs imports the reader at load.

On an index export: the eight suites 24/90/43/17/14/15/63/18,
conversation and control-board 153/153, webui 9/9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 16:36:20 -05:00

1175 lines
59 KiB
JavaScript

import { test, after } from "node:test";
import assert from "node:assert/strict";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
readFileSync,
existsSync,
chmodSync,
statSync,
symlinkSync,
lstatSync,
readdirSync,
} 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 { request as httpRequest } from "node:http";
import { ConfigError, markSeen } from "../src/scan.mjs";
import { isLoopbackHost, startServer, DEFAULT_AGENT_SEND, REPLY_LIMIT, REPLY_TRAILER, REFUSAL_STATUS } from "../src/serve.mjs";
import { writeRegistration, makeRegistration } from "../../seat/src/seat.mjs";
import { UNSUPPORTED_HARNESS } from "../../conversation/src/reader.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 = "<html><body>injected test page</body></html>";
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, "idle");
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: "<html></html>",
});
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: "<html></html>",
});
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>&\''), "&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;&amp;&#39;");
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: ["Input needed: Confirm the fixture."] }),
]);
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: "<html></html>" });
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: "<html></html>" });
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: "<html></html>" });
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: "<html></html>" });
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: "<html></html>" });
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: "<html></html>" });
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: ["Input needed: Confirm the fixture."] }),
].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, /<details id="seenDetails">(?![^>]*\sopen)/, "the Seen section must start collapsed");
assert.match(html, /<h2 id="seen-h">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, /<span>\(' \+ headCount \+ "\)<\/span>/, "the header uses headCount, not the raw group length");
assert.doesNotMatch(body, /<span>\(' \+ 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*'<span class="unknown">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, /<dt>Task<\/dt>/, "detail lists the task");
assert.match(body, /<dt>Active project<\/dt>/, "detail lists the active project");
assert.match(body, /<dt>Workspace<\/dt><dd>" \+ \(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(/<th scope=\\"col\\">Task<\/th><th scope=\\"col\\">Active project<\/th>/g) || [];
assert.equal(heads.length, 3, "all three tables carry the two new headers");
assert.match(html, /<td colspan="6" class="empty">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],
/<dt>Registered<\/dt><dd>" \+ 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], /<dt>Model<\/dt><dd>" \+ 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 <host>: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: "<html></html>", 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 <socket>", 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: "<html></html>", 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: "<html></html>", 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: "<html></html>", 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: "<html></html>", 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 '<span class="unknown">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], /<dt>Reply<\/dt><dd>" \+ 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");
});
// ---------------------------------------------------------------------------
// Task attribution (#1511): serialized by the server, rendered by page.html.
// ---------------------------------------------------------------------------
test("startServer: /api/board carries taskSetBy from a live registration and null for the derived rows", async () => {
const root = makeRoot();
const sessionsDir = join(root, "sessions");
const derivedDir = join(root, "derived-sessions");
for (const dir of [sessionsDir, derivedDir]) {
writeSessionFile(dir, "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 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, taskSetBy: "darkwing" });
const specs = [
{ agent: "agent1", project: "proj", sessionsDir, tmux: {} },
{ agent: "agent2", project: "proj", sessionsDir: derivedDir, tmux: {} },
];
const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir: join(root, "board"), seatsDir, isAlive: () => ({ alive: true, workspace: null }), page: "<html></html>" });
try {
const body = await (await fetch(`http://127.0.0.1:${server.address().port}/api/board`)).json();
const byAgent = Object.fromEntries(body.sessions.map((r) => [r.agent, r]));
assert.equal(byAgent.agent1.taskSource, "registration");
assert.equal(byAgent.agent1.taskSetBy, "darkwing");
assert.equal(byAgent.agent2.taskSource, "first-user-message");
assert.equal(byAgent.agent2.taskSetBy, null);
assert.deepEqual(body.registrationErrors, []);
} finally {
await new Promise((r) => server.close(r));
}
});
test("page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it", () => {
const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8");
const lines = html.split("\n");
const rowPair = html.match(/function buildRowPair\(rec, showProject\) \{[\s\S]*?\n \}/);
assert.ok(rowPair);
assert.match(rowPair[0], /sourceTag\(rec\.taskSource\) \+ setByTag\(rec\)/, "the set-by tag follows the task's source tag on the row");
assert.match(rowPair[0], /<dt>Task set by<\/dt><dd>" \+ setByText\(rec\) \+ "<\/dd>/, "the detail has a Task set by row");
const tagLine = lines.find((l) => l.includes("function setByTag(rec)"));
assert.ok(tagLine);
assert.match(tagLine, /rec\.taskSetBy \?/, "nothing is shown without a value");
assert.equal((tagLine.match(/esc\(rec\.taskSetBy\)/g) || []).length, 2, "the title and the text are both escaped");
assert.match(tagLine, /not verified/);
const textLine = lines.find((l) => l.includes("function setByText(rec)"));
assert.ok(textLine);
assert.match(textLine, /esc\(rec\.taskSetBy\)/);
assert.match(textLine, /not verified/);
// Attribution never enables a reply: canReply() reads the registration only.
const canReplyFn = html.match(/function canReply\(rec\) \{[\s\S]*?\n \}/);
assert.ok(canReplyFn);
assert.equal(canReplyFn[0].includes("taskSetBy"), false);
assert.equal(html.match(/function replyControl\(rec\) \{[\s\S]*?\n \}/)[0].includes("taskSetBy"), false);
});
// ---------------------------------------------------------------------------
// 11. Host and Origin guard (#1507). Binding to loopback does not stop a page
// whose DNS name was rebound to 127.0.0.1: the browser then treats this
// server as that page's own origin and sends its own name as Host. Every
// route refuses a Host that is not a loopback name on this port, and any
// Origin other than this server's own. Same check as the WebUI server.
// ---------------------------------------------------------------------------
// fetch() will not send a chosen Host header, so these requests use node:http.
function rawRequest(port, { method = "GET", path = "/api/board", headers = {}, body = null }) {
return new Promise((resolvePromise, reject) => {
const req = httpRequest({ host: "127.0.0.1", port, method, path, headers, setHost: false }, (res) => {
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => resolvePromise({ status: res.statusCode, headers: res.headers, text: Buffer.concat(chunks).toString("utf8") }));
});
req.on("error", reject);
req.end(body ?? undefined);
});
}
test("Host/Origin guard: GET /api/board and POST /api/reply refuse a foreign Host, a wrong port and a cross-origin Origin with 403 JSON, before any scan or send, and never send CORS headers", 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: "<html></html>", agentSend: f.agentSend });
const port = server.address().port;
const own = `127.0.0.1:${port}`;
const reply = JSON.stringify({ agent: "proj/agent1", text: "rebound page" });
const cases = [
["foreign Host", { host: `rebind.example:${port}` }, "non-local Host refused"],
["loopback Host, wrong port", { host: `127.0.0.1:${port + 1}` }, "non-local Host refused"],
["Host with credentials", { host: `x@${own}` }, "non-local Host refused"],
["cross-origin Origin", { host: own, origin: "http://rebind.example" }, "cross-origin request refused"],
["opaque Origin", { host: own, origin: "null" }, "cross-origin request refused"],
];
try {
for (const [label, headers, error] of cases) {
const board = await rawRequest(port, { headers });
assert.equal(board.status, 403, `GET /api/board, ${label}`);
assert.deepEqual(JSON.parse(board.text), { error }, `GET /api/board, ${label}`);
assert.equal(board.headers["access-control-allow-origin"], undefined);
const posted = await rawRequest(port, { method: "POST", path: "/api/reply", headers: { ...headers, "content-type": "application/json" }, body: reply });
assert.equal(posted.status, 403, `POST /api/reply, ${label}`);
assert.deepEqual(JSON.parse(posted.text), { error }, `POST /api/reply, ${label}`);
assert.equal(posted.headers["access-control-allow-origin"], undefined);
}
// Node's HTTP server answers an HTTP/1.1 request with no Host 400 before the handler runs.
assert.equal((await rawRequest(port, {})).status, 400, "GET /api/board, missing Host");
assert.equal((await rawRequest(port, { method: "POST", path: "/api/reply", headers: { "content-type": "application/json" }, body: reply })).status, 400, "POST /api/reply, missing Host");
assert.equal(existsSync(f.capture), false, "agent-send was never run");
assert.equal(existsSync(join(f.boardDir, "index.json")), false, "no refused request rescanned the board");
for (const path of ["/", "/healthz"]) {
assert.equal((await rawRequest(port, { path, headers: { host: `rebind.example:${port}` } })).status, 403, path);
}
// Refusals come first on the other routes too: no method or body handling.
assert.equal((await rawRequest(port, { method: "POST", path: "/api/seen", headers: { host: `rebind.example:${port}`, "content-type": "application/json" }, body: "{}" })).status, 403);
} finally {
await closeServer(server);
}
});
test("Host/Origin guard: loopback names on this port are accepted, with or without a same-origin Origin", 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: "<html></html>", agentSend: f.agentSend });
const port = server.address().port;
delete process.env.FAKE_SEND_EXIT;
delete process.env.FAKE_SEND_STDERR;
try {
for (const host of [`127.0.0.1:${port}`, `localhost:${port}`, `LOCALHOST:${port}`, `[::1]:${port}`]) {
assert.equal((await rawRequest(port, { headers: { host } })).status, 200, host);
assert.equal((await rawRequest(port, { headers: { host, origin: `http://${host}` } })).status, 200, `${host} with its own Origin`);
}
// The board's own page posts with its own Origin; the WebUI proxy (Node fetch) sends none.
const own = `127.0.0.1:${port}`;
const posted = await rawRequest(port, { method: "POST", path: "/api/reply", headers: { host: own, origin: `http://${own}`, "content-type": "application/json" }, body: JSON.stringify({ agent: "proj/agent1", text: "same origin" }) });
assert.equal(posted.status, 200);
assert.equal(JSON.parse(posted.text).delivered, true);
assert.equal((await fetch(`http://${own}/api/board`)).status, 200, "fetch without Origin");
} finally {
await closeServer(server);
}
});
// ---------------------------------------------------------------------------
// 12. Read-only conversation routes (#1507, CHAT-02 D3): GET /api/conversations
// and GET /api/conversation, served from packages/conversation. Same Host and
// Origin guard as every route (F16), application/json with nosniff and
// no-store, refusals mapped to 4xx with their code, and nothing written.
// ---------------------------------------------------------------------------
function conversationFixture() {
const root = makeRoot();
const proj = join(root, "proj");
const sessionsDir = join(proj, ".pi", "state", "agent1", "sessions");
const header = { type: "session", version: 3, id: "c0ffee00-0000-4000-8000-000000000001", timestamp: "2026-09-26T12:00:00.000Z", cwd: proj };
const lines = [header];
for (let i = 0; i < 120; i++) {
lines.push({ type: "message", id: `e${i}`, parentId: i ? `e${i - 1}` : null, timestamp: new Date(Date.parse("2026-09-26T12:00:01Z") + i * 1000).toISOString(), message: { role: i % 2 ? "assistant" : "user", content: [{ type: "text", text: `m${i}` }] } });
}
writeSessionFile(sessionsDir, "s.jsonl", lines.map((l) => JSON.stringify(l)));
const outside = join(root, "outside.jsonl");
writeFile(outside, JSON.stringify({ ...header, cwd: "/elsewhere" }) + "\n");
chmodSync(outside, 0o000);
symlinkSync(outside, join(sessionsDir, "link.jsonl"));
const seatsDir = join(root, "seats");
writeRegistration(seatsDir, makeRegistration({
resolved: { seat: "rocko", project: "proj", sessionsDir: join(proj, ".pi", "state", "rocko", "sessions"), seatDir: join(root, "rocko"), launchScript: join(root, "rocko", "launch.sh"), layout: "repo", defaultWorkspace: null },
harness: "claude-code",
}));
return { root, proj, sessionsDir, seatsDir, boardDir: join(root, "board"), specs: [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }] };
}
// Size, sha256, mtime, (dev, ino) and listing of every entry under dir.
function treePrint(dir) {
const out = {};
for (const name of readdirSync(dir).sort()) {
const path = join(dir, name);
const st = lstatSync(path, { bigint: true });
out[name] = [String(st.dev), String(st.ino), String(st.size), String(st.mtimeNs), st.isFile() ? createHashHex(readFileSync(path)) : null];
}
return out;
}
function createHashHex(buf) {
return spawnSync("sha256sum", { input: buf, encoding: "utf8" }).stdout.split(" ")[0];
}
test("conversation routes (F16): a foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers", async () => {
const f = conversationFixture();
const calls = [];
const spy = { catalogue: () => calls.push("catalogue"), open: () => calls.push("open"), next: () => calls.push("next") };
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>", conversationReader: spy });
const port = server.address().port;
const own = `127.0.0.1:${port}`;
const cases = [
["foreign Host", { host: `rebind.example:${port}` }, "non-local Host refused"],
["loopback Host, wrong port", { host: `127.0.0.1:${port + 1}` }, "non-local Host refused"],
["cross-origin Origin", { host: own, origin: "http://rebind.example" }, "cross-origin request refused"],
["opaque Origin", { host: own, origin: "null" }, "cross-origin request refused"],
];
try {
for (const path of ["/api/conversations", "/api/conversation?id=pi-00000000000000000000000000000000", "/api/conversation?id=x&cursor=c-1"]) {
for (const [label, headers, error] of cases) {
const r = await rawRequest(port, { path, headers });
assert.equal(r.status, 403, `${path}, ${label}`);
assert.deepEqual(JSON.parse(r.text), { error }, `${path}, ${label}`);
for (const h of Object.keys(r.headers)) assert.ok(!h.startsWith("access-control-"), `${path}, ${label}: ${h}`);
}
}
assert.deepEqual(calls, [], "the reader never ran for a refused request");
assert.equal((await rawRequest(port, { path: "/api/conversations", headers: { host: own, origin: `http://${own}` } })).status, 200, "same-origin passes");
assert.deepEqual(calls, ["catalogue"]);
} finally {
await closeServer(server);
}
});
test("every refusal code the reader can raise has an HTTP status", () => {
const src = ["reader.mjs", "pi.mjs", "safe-fs.mjs"].map((f) => readFileSync(join(pkgRoot, "..", "conversation", "src", f), "utf8")).join("\n");
const codes = new Set([...src.matchAll(/new Refusal\(\s*"([a-z-]+)"/g)].map((m) => m[1]));
codes.add(UNSUPPORTED_HARNESS); // raised by value, as a root's unsupportedReason
assert.ok(codes.size >= 15, [...codes].join(" "));
assert.deepEqual([...codes].filter((c) => !(c in REFUSAL_STATUS)), []);
assert.deepEqual(Object.keys(REFUSAL_STATUS).filter((c) => !codes.has(c)), [], "no stale entries");
});
test("conversation routes: catalogue, first page, next page and follow over HTTP; refusals map to 4xx with their code; nothing is written", async () => {
const f = conversationFixture();
const before = treePrint(f.sessionsDir);
const server = await startServer({ host: "127.0.0.1", port: 0, specs: f.specs, boardDir: f.boardDir, seatsDir: f.seatsDir, isAlive: () => true, page: "<html></html>" });
const base = `http://127.0.0.1:${server.address().port}`;
const get = async (path) => {
const res = await fetch(base + path);
assert.equal(res.headers.get("content-type"), "application/json", path);
assert.equal(res.headers.get("x-content-type-options"), "nosniff", path);
assert.equal(res.headers.get("cache-control"), "no-store", path);
assert.equal(res.headers.get("access-control-allow-origin"), null, path);
return { status: res.status, body: await res.json() };
};
try {
const cat = await get("/api/conversations");
assert.equal(cat.status, 200);
const rows = cat.body.conversations;
const pi = rows.find((c) => c.availability === "available");
const link = rows.find((c) => c.availability === "denied");
const claude = rows.find((c) => c.availability === "unsupported");
assert.deepEqual([pi.seat, pi.title, pi.readOnly], ["agent1", "m0", true]);
assert.deepEqual([link.refusal, claude.seat, claude.unsupportedReason], ["unsafe-path", "rocko", "unsupported-harness"]);
const first = await get(`/api/conversation?id=${pi.conversation}`);
assert.equal(first.status, 200);
assert.deepEqual([first.body.page.kind, first.body.page.entries.length, first.body.page.hasMore, first.body.page.branch], ["page", 100, true, "main"]);
const q = (cursor) => `/api/conversation?id=${pi.conversation}&branch=${first.body.page.branch}&cursor=${cursor}`;
const second = await get(q(first.body.page.nextCursor));
assert.equal(second.status, 200);
assert.deepEqual([second.body.page.entries.length, second.body.page.hasMore], [20, false]);
assert.equal(second.body.page.entries.at(-1).content[0].text, "m119");
const follow = await get(q(second.body.follow.id));
assert.deepEqual([follow.status, follow.body.page.entries.length], [200, 0]);
const branch = await get(`/api/conversation?id=${pi.conversation}&branch=main`);
assert.equal(branch.status, 200);
const refusals = [
[`/api/conversation?id=pi-${"0".repeat(32)}`, 404, "unknown-conversation", true],
[`/api/conversation?id=${pi.conversation}&branch=b.e5`, 404, "unknown-branch", true],
[q("c-unknown"), 409, "cursor-unknown", true],
[`/api/conversation?id=${pi.conversation}&branch=b.e1&cursor=${first.body.page.nextCursor}`, 409, "cursor-foreign", true],
[`/api/conversation?id=${link.conversation}`, 403, "unsafe-path", false],
[`/api/conversation?id=${claude.conversation}`, 422, "unsupported-harness", false],
];
for (const [path, status, code, reconcile] of refusals) {
const r = await get(path);
assert.equal(r.status, status, path);
assert.deepEqual(r.body.refusal, { code, reconcile }, path);
assert.equal(typeof r.body.error, "string");
}
for (const path of ["/api/conversation", "/api/conversation?id=a&id=b", "/api/conversation?id=a&path=/etc/passwd", "/api/conversation?id=a&path=x", `/api/conversation?id=${pi.conversation}&cursor=${second.body.follow.id}`, "/api/conversation?id=../x", `/api/conversation?id=${"a".repeat(129)}`, "/api/conversations?x=1"]) {
const r = await get(path);
assert.equal(r.status, 400, path);
}
assert.deepEqual(treePrint(f.sessionsDir), before, "the sessions directory is unchanged");
assert.equal(existsSync(join(f.proj, ".pi", "state", "rocko")), false, "the Claude seat's directory was never created");
} finally {
await closeServer(server);
}
});