import { test, after } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync, readFileSync, readdirSync, existsSync, statSync, symlinkSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve, basename } from "node:path"; import { spawnSync } from "node:child_process"; import { loadConfig, ConfigError, findNewestSession, readSession, deriveState, scanAgent, discoverRepoAgents, discoverFleetAgents, scan, panesRunPi, parsePanes, tmuxInspect, tmuxIsAlive, findRepoRoot, loadSeen, markSeen, loadRegistrations, matchRegistration, pidAlive, } from "../src/scan.mjs"; import { writeRegistration, makeRegistration } from "../../seat/src/seat.mjs"; const pkgRoot = resolve(import.meta.dirname, ".."); const cli = join(pkgRoot, "src", "cli.mjs"); // Track every tmpdir we create so a stray failure never leaves fixtures behind. const roots = []; function makeRoot() { const root = mkdtempSync(join(tmpdir(), "control-board-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, blocks, model, provider }) { const message = { role }; if (stopReason !== undefined) message.stopReason = stopReason; if (model !== undefined) message.model = model; if (provider !== undefined) message.provider = provider; if (texts) message.content = texts.map((text) => ({ type: "text", text })); if (blocks) message.content = (message.content || []).concat(blocks); return JSON.stringify({ type: "message", timestamp, message }); } const TOOL_CALL = { type: "toolCall", id: "call_1", name: "bash", arguments: { command: "ls" } }; // Same transform readSession applies, kept here as an independent // expectation rather than importing an unexported helper. function collapseExpected(text) { const one = text.replace(/\s+/g, " ").trim(); return one.length > 240 ? one.slice(0, 239) + "…" : one; } function writeSessionFile(dir, name, lines, { trailingNewline = true } = {}) { const path = join(dir, name); writeFile(path, lines.join("\n") + (trailingNewline ? "\n" : "")); return path; } // --------------------------------------------------------------------------- // 1. loadConfig fail-closed // --------------------------------------------------------------------------- test("loadConfig: missing file throws ConfigError", () => { const root = makeRoot(); assert.throws(() => loadConfig(join(root, "nope.json")), ConfigError); }); test("loadConfig: invalid JSON throws ConfigError", () => { const root = makeRoot(); const path = join(root, "config.json"); writeFile(path, "{ not json"); assert.throws(() => loadConfig(path), ConfigError); }); test("loadConfig: missing dataRoot throws ConfigError", () => { const root = makeRoot(); const path = join(root, "config.json"); writeFile(path, JSON.stringify({})); assert.throws(() => loadConfig(path), ConfigError); }); test("loadConfig: relative dataRoot throws ConfigError", () => { const root = makeRoot(); const path = join(root, "config.json"); writeFile(path, JSON.stringify({ dataRoot: "relative/path" })); assert.throws(() => loadConfig(path), ConfigError); }); test("loadConfig: valid config returns dataRoot", () => { const root = makeRoot(); const path = join(root, "config.json"); const dataRoot = join(root, "data"); writeFile(path, JSON.stringify({ dataRoot })); assert.deepEqual(loadConfig(path), { dataRoot }); }); // --------------------------------------------------------------------------- // 2. findNewestSession // --------------------------------------------------------------------------- test("findNewestSession: picks the newest by mtime among two files", () => { const root = makeRoot(); const older = join(root, "older.jsonl"); const newer = join(root, "newer.jsonl"); writeFile(older, "{}\n"); writeFile(newer, "{}\n"); utimesSync(older, new Date("2026-01-01T00:00:00Z"), new Date("2026-01-01T00:00:00Z")); utimesSync(newer, new Date("2026-01-02T00:00:00Z"), new Date("2026-01-02T00:00:00Z")); assert.equal(findNewestSession(root), newer); }); test("findNewestSession: finds files in nested subdirectories", () => { const root = makeRoot(); const nested = join(root, "a", "b"); mkdirSync(nested, { recursive: true }); const top = join(root, "top.jsonl"); const deep = join(nested, "deep.jsonl"); writeFile(top, "{}\n"); writeFile(deep, "{}\n"); utimesSync(top, new Date("2026-01-01T00:00:00Z"), new Date("2026-01-01T00:00:00Z")); utimesSync(deep, new Date("2026-01-05T00:00:00Z"), new Date("2026-01-05T00:00:00Z")); assert.equal(findNewestSession(root), deep); }); test("findNewestSession: returns null for a missing dir", () => { const root = makeRoot(); assert.equal(findNewestSession(join(root, "does-not-exist")), null); }); // --------------------------------------------------------------------------- // 3. readSession // --------------------------------------------------------------------------- test("readSession: extracts fields, collapses/truncates text, counts a truncated final line", () => { const root = makeRoot(); const longText = " Field report: " + "alpha ".repeat(60) + " omega "; const expectedText = collapseExpected(["First part.", longText].join("\n")); assert.ok(expectedText.length === 240 && expectedText.endsWith("…"), "fixture text must exceed the 240-char limit"); const lines = [ sessionLine({ id: "sess-1", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/x" }), messageLine({ timestamp: "2026-09-01T00:05:00.000Z", role: "user", texts: ["hi"] }), messageLine({ timestamp: "2026-09-01T00:10:00.000Z", role: "assistant", stopReason: "stop", texts: ["First part.", longText], }), // Truncated / partially-written final line: not valid JSON, no trailing newline. '{"type":"message","timestamp":"2026-09-01T00:15:00.000Z","message":{"role":"assistant","stopReaso', ]; const path = writeSessionFile(root, "session.jsonl", lines, { trailingNewline: false }); const result = readSession(path); assert.equal(result.file, path); assert.equal(result.sessionId, "sess-1"); assert.equal(result.cwd, "/x"); assert.equal(result.lastTimestamp, "2026-09-01T00:10:00.000Z"); assert.equal(result.lastMessage.role, "assistant"); assert.equal(result.lastMessage.stopReason, "stop"); assert.equal(result.lastAssistantText, expectedText); assert.equal(result.lastAssistantText.length, 240); assert.ok(result.lastAssistantText.endsWith("…")); assert.equal(result.skippedLines, 1); }); test("readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them", () => { const root = makeRoot(); const modelChange = (timestamp, provider, modelId) => JSON.stringify({ type: "model_change", id: "m", parentId: null, timestamp, provider, modelId }); const switchedDir = join(root, "switched"); writeSessionFile(switchedDir, "model.jsonl", [ sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }), modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"), messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }), messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "glm-5.3-flash", provider: "zai" }), modelChange("2026-09-12T16:08:52.233Z", "openai-codex", "gpt-6-astra"), ]); const switched = readSession(join(switchedDir, "model.jsonl")); assert.equal(switched.model, "gpt-6-astra", "a /model switch after the last answer wins"); assert.equal(switched.provider, "openai-codex"); const answered = writeSessionFile(root, "answered.jsonl", [ sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }), modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"), messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "claude-sonnet-5", provider: "anthropic" }), messageLine({ timestamp: "2026-09-12T16:07:06Z", role: "user", texts: ["and?"] }), ]); assert.deepEqual([readSession(answered).model, readSession(answered).provider], ["claude-sonnet-5", "anthropic"], "the assistant turn names the model that produced it"); const noneDir = join(root, "none"); writeSessionFile(noneDir, "none.jsonl", [ sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }), messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }), ]); assert.equal(readSession(join(noneDir, "none.jsonl")).model, null); assert.equal(readSession(join(noneDir, "none.jsonl")).provider, null); const rec = scanAgent({ agent: "a", project: "p", sessionsDir: switchedDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW }); assert.equal(rec.model, "gpt-6-astra", "scanAgent carries model through from the newest log"); assert.equal(rec.provider, "openai-codex"); const bare = scanAgent({ agent: "a", project: "p", sessionsDir: noneDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW }); assert.equal(bare.model, null); assert.equal(bare.provider, null); }); test("readSession: lastError carries the assistant errorMessage only when the last assistant turn errored", () => { const root = makeRoot(); const errored = JSON.stringify({ type: "message", timestamp: "2026-09-01T00:10:00.000Z", message: { role: "assistant", stopReason: "error", errorMessage: "429: usage limit\nreached", content: [] }, }); const path = writeSessionFile(root, "err.jsonl", [ sessionLine({ id: "sess-e", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/x" }), messageLine({ timestamp: "2026-09-01T00:05:00.000Z", role: "assistant", stopReason: "stop", texts: ["fine"] }), errored, ]); const result = readSession(path); assert.equal(result.lastError, "429: usage limit reached"); assert.equal(result.lastAssistantText, "fine"); const recovered = writeSessionFile(root, "ok.jsonl", [ sessionLine({ id: "sess-o", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/x" }), errored, messageLine({ timestamp: "2026-09-01T00:15:00.000Z", role: "assistant", stopReason: "stop", texts: ["back"] }), ]); assert.equal(readSession(recovered).lastError, null); const rec = scanAgent({ agent: "a", project: "p", sessionsDir: root, tmux: { socket: null, session: "a" } }, { isAlive: () => true }); assert.equal(rec.lastError, null); }); test("findNewestSession/scan: never read sibling auth or secrets next to a sessions dir", () => { const root = makeRoot(); const agentDir = join(root, "agent-x", ".pi", "agent"); const sessionsDir = join(agentDir, "sessions", "nested"); writeFile(join(agentDir, "auth.json"), '{"token":"SECRET-AUTH-TOKEN"}'); writeFile(join(root, "agent-x", "secrets", "seat.json"), '{"token":"SECRET-SEAT-TOKEN"}'); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "sess-s", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/x" }), messageLine({ timestamp: "2026-09-01T00:05:00.000Z", role: "assistant", stopReason: "stop", texts: ["ok"] }), ]); assert.equal(findNewestSession(join(agentDir, "sessions")), join(sessionsDir, "s.jsonl")); const boardDir = join(root, "board"); scan([{ agent: "agent-x", project: "p", sessionsDir: join(agentDir, "sessions"), tmux: { socket: null, session: "agent-x" } }], { boardDir, isAlive: () => true }); const written = [readFileSync(join(boardDir, "index.json"), "utf8"), readFileSync(join(boardDir, "sessions", "p", "agent-x.json"), "utf8")].join("\n"); assert.ok(!written.includes("SECRET-"), "board output must not contain secret material"); assert.ok(!written.includes("auth.json") && !written.includes("secrets/"), "board output must not reference auth or secrets paths"); }); // --------------------------------------------------------------------------- // 4. deriveState table // --------------------------------------------------------------------------- test("deriveState: full state table", () => { const cases = [ { name: "alive=false is offline regardless of session", input: { alive: false, session: { lastMessage: { role: "assistant", stopReason: "stop" } } }, expected: "offline" }, { name: "no session is idle", input: { alive: true, session: null }, expected: "idle" }, { name: "no lastMessage is idle", input: { alive: true, session: { lastMessage: null } }, expected: "idle" }, { name: "assistant+stop is waiting", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "stop" } } }, expected: "waiting" }, { name: "assistant+error is error", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "error" } } }, expected: "error" }, { name: "assistant+aborted is error", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "aborted" } } }, expected: "error" }, { name: "assistant+toolUse is working", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "toolUse" } } }, expected: "working" }, { name: "assistant with a toolCall block is working even if stopReason says stop", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "stop", content: [{ type: "text", text: "Shall I?" }, TOOL_CALL] } } }, expected: "working" }, { name: "assistant+error with a toolCall block is still error", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "error", content: [TOOL_CALL] } } }, expected: "error" }, { name: "assistant+stop with thinking and text (no tool call) is waiting", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "stop", content: [{ type: "thinking", thinking: "hm" }, { type: "text", text: "done" }] } } }, expected: "waiting" }, { name: "user last is working", input: { alive: true, session: { lastMessage: { role: "user" } } }, expected: "working" }, { name: "toolResult last is working", input: { alive: true, session: { lastMessage: { role: "toolResult" } } }, expected: "working" }, { name: "assistant+length (cut off) is error", input: { alive: true, session: { lastMessage: { role: "assistant", stopReason: "length" } } }, expected: "error" }, { name: "alive=null (liveness not checkable) is unknown, never assumed alive", input: { alive: null, session: { lastMessage: { role: "assistant", stopReason: "stop" } } }, expected: "unknown" }, ]; for (const c of cases) { assert.equal(deriveState(c.input), c.expected, c.name); } }); // --------------------------------------------------------------------------- // 4b. Acceptance rule: a seat mid-tool-call is working, never waiting // (docs/plans/2026-09-12_control-board-mvp.md). Fixtures mirror real pi logs. // --------------------------------------------------------------------------- function ruleSpec(root) { return { agent: "a", project: "p", sessionsDir: join(root, "sessions"), tmux: {} }; } test("rule: newest entry is an assistant message with a tool call, after a question-looking text, is working", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["go"] }), messageLine({ timestamp: "2026-09-12T14:00:02Z", role: "assistant", stopReason: "toolUse", texts: ["Should I run the suite now?"], blocks: [TOOL_CALL] }), ]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: () => new Date("2026-09-12T14:00:10Z") }); assert.equal(rec.state, "working"); assert.equal(rec.waitingOnYou, false); assert.equal(rec.lastAssistantText, "Should I run the suite now?", "the question is still shown, it just does not mean waiting"); }); test("rule: newest entry is a tool result with no assistant text after it is working", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:02Z", role: "assistant", stopReason: "toolUse", blocks: [{ type: "thinking", thinking: "x" }, TOOL_CALL] }), messageLine({ timestamp: "2026-09-12T14:00:03Z", role: "toolResult", texts: ["file1\nfile2"] }), ]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: () => new Date("2026-09-12T14:00:10Z") }); assert.equal(rec.state, "working"); assert.equal(rec.waitingOnYou, false); }); test("rule: a finished turn (text-only assistant message, stopReason stop) is waiting", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:02Z", role: "assistant", stopReason: "toolUse", blocks: [TOOL_CALL] }), messageLine({ timestamp: "2026-09-12T14:00:03Z", role: "toolResult", texts: ["ok"] }), messageLine({ timestamp: "2026-09-12T14:00:04Z", role: "assistant", stopReason: "stop", texts: ["Done. Your move."] }), ]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: () => new Date("2026-09-12T14:00:10Z") }); assert.equal(rec.state, "waiting"); assert.equal(rec.waitingOnYou, true); }); // --------------------------------------------------------------------------- // 4c. Gate A: task / workspace / active project per row, derived or "unknown" // (docs/plans/2026-09-12_control-board-mvp.md, Step 3 log). One fixture per field. // --------------------------------------------------------------------------- const GATE_NOW = () => new Date("2026-09-12T14:00:10Z"); test("task: the first user message of the session, from text blocks", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["[orch-01 -> code-be-01 class=actionable] T-H2-RE: fix #1466"] }), messageLine({ timestamp: "2026-09-12T14:00:02Z", role: "assistant", stopReason: "stop", texts: ["On it."] }), messageLine({ timestamp: "2026-09-12T14:00:03Z", role: "user", texts: ["and then report back"] }), messageLine({ timestamp: "2026-09-12T14:00:04Z", role: "assistant", stopReason: "stop", texts: ["Done."] }), ]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: GATE_NOW }); assert.equal(rec.task, "[orch-01 -> code-be-01 class=actionable] T-H2-RE: fix #1466"); assert.equal(rec.taskSource, "first-user-message"); }); test("task: a plain-string user content is accepted, whitespace collapsed and long text capped", () => { const root = makeRoot(); const long = "orchestrate " + "x".repeat(300); const line = JSON.stringify({ type: "message", timestamp: "2026-09-12T14:00:01Z", message: { role: "user", content: " " + long + "\n\n" } }); writeSessionFile(join(root, "sessions"), "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), line, ]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: GATE_NOW }); assert.equal(rec.task, collapseExpected(long)); }); test("task: no user message in the log means null (shown as unknown), never a guess", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" })]); const rec = scanAgent(ruleSpec(root), { isAlive: () => true, now: GATE_NOW }); assert.equal(rec.task, null); assert.equal(rec.taskSource, null); const none = scanAgent(ruleSpec(makeRoot()), { isAlive: () => true, now: GATE_NOW }); assert.equal(none.task, null); }); test("workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null", () => { const root = makeRoot(); writeSessionFile(join(root, "sessions"), "s.jsonl", [sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/from/log" })]); const fromTmux = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: true, workspace: "/from/tmux" }), now: GATE_NOW }); assert.equal(fromTmux.workspace, "/from/tmux"); assert.equal(fromTmux.workspaceSource, "tmux-pane"); assert.equal(fromTmux.alive, true); assert.equal(fromTmux.cwd, "/from/log"); const fromLog = scanAgent(ruleSpec(root), { isAlive: () => true, now: GATE_NOW }); assert.equal(fromLog.workspace, "/from/log"); assert.equal(fromLog.workspaceSource, "session-cwd"); const offline = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: false, workspace: null }), now: GATE_NOW }); assert.equal(offline.state, "offline"); assert.equal(offline.workspace, "/from/log"); const nothing = scanAgent(ruleSpec(makeRoot()), { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); assert.equal(nothing.workspace, null); assert.equal(nothing.workspaceSource, null); }); test("activeProject: basename of the nearest .git directory or .git file above the workspace; none means null", () => { const root = makeRoot(); const repo = join(root, "repos", "my-repo"); mkdirSync(join(repo, ".git"), { recursive: true }); mkdirSync(join(repo, "packages", "deep"), { recursive: true }); const worktree = join(root, "repos", "my-worktree"); mkdirSync(join(worktree, "sub"), { recursive: true }); writeFileSync(join(worktree, ".git"), "gitdir: /elsewhere\n"); const plain = join(root, "plain", "dir"); mkdirSync(plain, { recursive: true }); assert.equal(findRepoRoot(join(repo, "packages", "deep")), repo); assert.equal(findRepoRoot(join(worktree, "sub")), worktree); assert.equal(findRepoRoot("relative/path"), null); const inRepo = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: true, workspace: join(repo, "packages", "deep") }), now: GATE_NOW }); assert.equal(inRepo.activeProject, "my-repo"); const inWorktree = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: true, workspace: join(worktree, "sub") }), now: GATE_NOW }); assert.equal(inWorktree.activeProject, "my-worktree"); // The group column (spec.project) is untouched; seen.json keys depend on it. assert.equal(inWorktree.project, "p"); const noRepo = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: true, workspace: plain }), now: GATE_NOW }); // A temp dir may sit under a git checkout on some machines; only assert when it does not. if (findRepoRoot(plain) === null) assert.equal(noRepo.activeProject, null); const noWorkspace = scanAgent(ruleSpec(root), { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); assert.equal(noWorkspace.activeProject, null); }); test("scan: the written record carries task, workspace and activeProject", () => { const root = makeRoot(); const boardDir = join(root, "board"); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["resume"] }), ]); const index = scan([{ agent: "a", project: "p", sessionsDir, tmux: {} }], { boardDir, isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); const rec = index.sessions[0]; assert.equal(rec.task, "resume"); assert.equal(rec.workspace, "/w"); assert.equal("activeProject" in rec, true); const onDisk = JSON.parse(readFileSync(join(boardDir, "sessions", "p", "a.json"), "utf8")); assert.equal(onDisk.task, "resume"); assert.equal(onDisk.workspace, "/w"); }); // --------------------------------------------------------------------------- // 4d. Registration (mosaic launch) // --------------------------------------------------------------------------- test("registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/derived/cwd" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived task"] }), ]); const spec = { agent: "a", project: "p", sessionsDir, tmux: {} }; const registration = makeRegistration({ resolved: { seat: "a", project: "resolved-project", sessionsDir, seatDir: join(root, "seat-dir"), launchScript: join(root, "seat-dir", "launch.sh"), layout: "repo", defaultWorkspace: "/resolved/workspace", }, task: "registered task", project: "registered-project", workspace: "/registered/workspace", harness: "pi", tmux: { socket: null, session: "a" }, pid: process.pid, now: () => new Date("2026-09-12T13:00:00Z"), }); const rec = scanAgent(spec, { isAlive: () => true, now: GATE_NOW, registration }); assert.equal(rec.task, "registered task"); assert.equal(rec.taskSource, "registration"); assert.equal(rec.activeProject, "registered-project"); assert.equal(rec.activeProjectSource, "registration"); assert.equal(rec.workspace, "/registered/workspace"); assert.equal(rec.workspaceSource, "registration"); assert.equal(rec.project, "p", "the grouping column (spec.project) is not touched by a registration"); assert.ok(rec.registered, "registered must be non-null once a matching registration is passed"); assert.equal(rec.registered.startedAt, registration.startedAt); assert.equal(rec.registered.harness, "pi"); assert.equal(rec.registered.pid, process.pid); assert.equal(rec.registered.alive, true, "the default pid probe sees this test process"); assert.deepEqual(rec.registered.tmux, { socket: null, session: "a" }); assert.equal(rec.registered.layout, "repo"); assert.equal(rec.registered.launchScript, join(root, "seat-dir", "launch.sh")); }); test("registration: empty task and null project/workspace leave the derived values in place; registered is still non-null", () => { const root = makeRoot(); const repo = join(root, "repo"); mkdirSync(join(repo, ".git"), { recursive: true }); const sessionsDir = join(repo, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: repo }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived task"] }), ]); const spec = { agent: "a", project: "p", sessionsDir, tmux: {} }; const registration = makeRegistration({ resolved: { seat: "a", project: null, sessionsDir, seatDir: repo, launchScript: join(repo, "launch.sh"), layout: "repo", defaultWorkspace: null, }, task: "", project: null, workspace: null, now: () => new Date("2026-09-12T13:00:00Z"), }); const rec = scanAgent(spec, { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW, registration }); assert.equal(rec.task, "derived task"); assert.equal(rec.taskSource, "first-user-message"); assert.equal(rec.activeProject, "repo"); assert.equal(rec.activeProjectSource, "workspace-git-root"); assert.equal(rec.workspace, repo); assert.equal(rec.workspaceSource, "session-cwd"); assert.notEqual(rec.registered, null, "an empty task and null project/workspace still leave a registration attached"); }); test("registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/derived/cwd" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived task"] }), ]); const spec = { agent: "a", project: "p", sessionsDir, tmux: {} }; const registration = makeRegistration({ resolved: { seat: "a", project: "resolved-project", sessionsDir, seatDir: join(root, "seat-dir"), launchScript: join(root, "seat-dir", "launch.sh"), layout: "repo", defaultWorkspace: "/resolved/workspace" }, task: "registered task", project: "registered-project", workspace: "/registered/workspace", harness: "pi", pid: 4242, }); const probed = []; const rec = scanAgent(spec, { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW, registration, isPidAlive: (pid) => { probed.push(pid); return false; } }); assert.deepEqual(probed, [4242], "the probe is asked about the recorded pid"); assert.equal(rec.task, "derived task"); assert.equal(rec.taskSource, "first-user-message"); assert.equal(rec.activeProject, null, "no git root above /derived/cwd"); assert.equal(rec.activeProjectSource, null); assert.equal(rec.workspace, "/derived/cwd"); assert.equal(rec.workspaceSource, "session-cwd"); assert.ok(rec.registered, "a stale record is still reported, not hidden"); assert.equal(rec.registered.alive, false); assert.equal(rec.registered.pid, 4242); assert.equal(rec.registered.startedAt, registration.startedAt); // null from the probe (no pid recorded) keeps the override. const undecided = scanAgent(spec, { isAlive: () => true, now: GATE_NOW, registration, isPidAlive: () => null }); assert.equal(undecided.task, "registered task"); assert.equal(undecided.registered.alive, null); // scan(): the stale row is listed under registrationStale and still under registered. const boardDir = join(root, "board"); const seatsDir = join(root, "seats"); writeRegistration(seatsDir, registration); const index = scan([spec], { boardDir, seatsDir, isAlive: () => true, now: GATE_NOW, isPidAlive: () => false }); assert.deepEqual(index.registered, ["p/a"]); assert.deepEqual(index.registrationStale, ["p/a"]); assert.equal(index.sessions[0].taskSource, "first-user-message"); assert.equal(pidAlive(process.pid), true); assert.equal(pidAlive(null), null); assert.equal(pidAlive(0), null); assert.equal(pidAlive(-1), null); }); test("registration: no registration leaves the Gate A fields exactly as before, and registered is null", () => { const root = makeRoot(); const repo = join(root, "repo"); mkdirSync(join(repo, ".git"), { recursive: true }); const sessionsDir = join(repo, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: repo }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived task"] }), ]); const spec = { agent: "a", project: "p", sessionsDir, tmux: {} }; const withoutReg = scanAgent(spec, { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); assert.equal(withoutReg.registered, null); assert.equal(withoutReg.task, "derived task"); assert.equal(withoutReg.taskSource, "first-user-message"); assert.equal(withoutReg.activeProject, "repo"); assert.equal(withoutReg.activeProjectSource, "workspace-git-root"); assert.equal(withoutReg.workspace, repo); assert.equal(withoutReg.workspaceSource, "session-cwd"); const plainRoot = makeRoot(); const plainSessions = join(plainRoot, "sessions"); writeSessionFile(plainSessions, "s.jsonl", [sessionLine({ id: "s2", timestamp: "2026-09-12T14:00:00Z", cwd: null })]); const noWorkspace = scanAgent({ agent: "a", project: "p", sessionsDir: plainSessions, tmux: {} }, { isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); assert.equal(noWorkspace.registered, null); assert.equal(noWorkspace.activeProjectSource, null, "no workspace at all means no repo root, so the source is null, not a guess"); }); test("loadRegistrations: a missing seatsDir gives empty lists", () => { const root = makeRoot(); assert.deepEqual(loadRegistrations(join(root, "no-such-seats")), { registrations: [], errors: [] }); assert.deepEqual(loadRegistrations(null), { registrations: [], errors: [] }); }); test("loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored", () => { const root = makeRoot(); const seatsDir = join(root, "seats"); const good = makeRegistration({ resolved: { seat: "good", project: "p", sessionsDir: join(root, "sessions"), seatDir: join(root, "good"), launchScript: join(root, "good", "launch.sh"), layout: "repo", defaultWorkspace: null, }, task: "do it", }); writeRegistration(seatsDir, good); mkdirSync(join(seatsDir, "repo", "broken"), { recursive: true }); writeFileSync(join(seatsDir, "repo", "broken", "registration.json"), "{ not json"); const unknownField = { ...good, seat: "weird", extraField: "surprise!!!" }; mkdirSync(join(seatsDir, "repo", "weird"), { recursive: true }); writeFileSync(join(seatsDir, "repo", "weird", "registration.json"), JSON.stringify(unknownField)); // A stray non-directory entry directly under seatsDir must be ignored, not // treated as a broken seat. writeFileSync(join(seatsDir, "not-a-seat.txt"), "stray file, not a seat directory"); writeFileSync(join(seatsDir, "repo", "not-a-seat.txt"), "stray file inside a layout directory"); // A record whose contents disagree with its path is an error, not a match. mkdirSync(join(seatsDir, "fleet", "good"), { recursive: true }); writeFileSync(join(seatsDir, "fleet", "good", "registration.json"), JSON.stringify(good)); const { registrations, errors } = loadRegistrations(seatsDir); assert.equal(registrations.length, 1); assert.equal(registrations[0].seat, "good"); assert.equal(errors.length, 3); assert.ok(errors.some((e) => e.startsWith("seat fleet/good:") && e.includes("does not match its path")), "a record under the wrong layout is reported"); assert.ok(errors.some((e) => e.startsWith("seat repo/broken:")), "the broken record's error names its seat"); assert.ok(errors.some((e) => e.startsWith("seat repo/weird:")), "the unknown-field record's error names its seat"); for (const e of errors) { assert.ok(!e.includes("surprise!!!"), "an error must not echo a record's field values"); assert.ok(!e.includes("not json"), "an error must not echo the raw file contents"); } }); test("matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing)", () => { const root = makeRoot(); const sessionsDir = join(root, "repo-sessions"); mkdirSync(sessionsDir, { recursive: true }); const spec = { agent: "darkwing", project: "repo-project", sessionsDir, tmux: {} }; const reg = makeRegistration({ resolved: { seat: "darkwing", project: null, sessionsDir, seatDir: root, launchScript: join(root, "launch.sh"), layout: "repo", defaultWorkspace: null, }, task: "", }); assert.equal(matchRegistration(spec, [reg]), reg); // The spec's sessionsDir differs textually from the registration's, but // resolves (via realpath) to the same real directory. const linkPath = join(root, "sessions-link"); symlinkSync(sessionsDir, linkPath); const specViaLink = { ...spec, sessionsDir: linkPath }; assert.equal(matchRegistration(specViaLink, [reg]), reg); // A registration with sessionsDir null (layout "unknown") never matches. const regNoSessions = { ...reg, sessionsDir: null }; assert.equal(matchRegistration(spec, [regNoSessions]), null); // Same seat name, different sessionsDir: the repo and fleet layouts can // both have a "darkwing", and they are different seats. const fleetSessions = join(root, "fleet-sessions"); mkdirSync(fleetSessions, { recursive: true }); const fleetReg = { ...reg, sessionsDir: fleetSessions }; assert.equal(matchRegistration(spec, [fleetReg]), null); }); test("scan: writes the registration override to disk; index.json carries registered and registrationErrors", () => { const root = makeRoot(); const boardDir = join(root, "board"); const sessionsDir = join(root, "sessions"); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-12T14:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-12T14:00:01Z", role: "user", texts: ["derived"] }), ]); const seatsDir = join(root, "seats"); const reg = makeRegistration({ resolved: { seat: "a", project: "p", sessionsDir, seatDir: join(root, "a"), launchScript: join(root, "a", "launch.sh"), layout: "repo", defaultWorkspace: null, }, task: "registered task", }); writeRegistration(seatsDir, reg); const spec = { agent: "a", project: "p", sessionsDir, tmux: {} }; const index = scan([spec], { boardDir, seatsDir, isAlive: () => ({ alive: true, workspace: null }), now: GATE_NOW }); assert.deepEqual(index.registered, ["p/a"]); assert.deepEqual(index.registrationStale, [], "a record with pid null is never stale"); assert.deepEqual(index.registrationErrors, []); const onDisk = JSON.parse(readFileSync(join(boardDir, "sessions", "p", "a.json"), "utf8")); assert.equal(onDisk.task, "registered task"); assert.equal(onDisk.taskSource, "registration"); assert.ok(onDisk.registered); }); test("scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before", () => { const root = makeRoot(); const boardDir = join(root, "board"); assert.throws(() => scan([], { boardDir, seatsDir: "relative/seats", isAlive: () => true, now: GATE_NOW }), ConfigError); const index = scan([], { boardDir, isAlive: () => true, now: GATE_NOW }); assert.deepEqual(index.registered, []); assert.deepEqual(index.registrationErrors, []); }); // --------------------------------------------------------------------------- // 5. scanAgent // --------------------------------------------------------------------------- test("scanAgent: waitingOnYou is true for waiting/error and false otherwise", () => { const root = makeRoot(); const waitingDir = join(root, "waiting", "sessions"); const workingDir = join(root, "working", "sessions"); const errorDir = join(root, "error", "sessions"); mkdirSync(waitingDir, { recursive: true }); mkdirSync(workingDir, { recursive: true }); mkdirSync(errorDir, { recursive: true }); writeSessionFile(waitingDir, "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"] }), ]); writeSessionFile(workingDir, "s.jsonl", [ sessionLine({ id: "s2", timestamp: "2026-09-01T00:00:00Z", cwd: "/k" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "toolUse", texts: ["thinking"] }), ]); writeSessionFile(errorDir, "s.jsonl", [ sessionLine({ id: "s3", timestamp: "2026-09-01T00:00:00Z", cwd: "/e" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "aborted" }), ]); const opts = { isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z") }; const waiting = scanAgent({ agent: "a", project: "p", sessionsDir: waitingDir, tmux: {} }, opts); const working = scanAgent({ agent: "a", project: "p", sessionsDir: workingDir, tmux: {} }, opts); const errored = scanAgent({ agent: "a", project: "p", sessionsDir: errorDir, tmux: {} }, opts); assert.equal(waiting.state, "waiting"); assert.equal(waiting.waitingOnYou, true); assert.equal(working.state, "working"); assert.equal(working.waitingOnYou, false); assert.equal(errored.state, "error"); assert.equal(errored.waitingOnYou, true); }); test("scanAgent: ageSeconds is computed from the injected now", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00.000Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:00.000Z", role: "assistant", stopReason: "stop", texts: ["done"] }), ]); const now = new Date(Date.parse("2026-09-01T00:00:00.000Z") + 90_000); const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => now }); assert.equal(rec.ageSeconds, 90); assert.equal(rec.scannedAt, now.toISOString()); }); test("scanAgent: sessionFile null and state idle when sessions dir is empty but alive", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => new Date() }); assert.equal(rec.sessionFile, null); assert.equal(rec.state, "idle"); assert.equal(rec.alive, true); assert.equal(rec.sessionId, null); assert.equal(rec.cwd, null); assert.equal(rec.lastAssistantText, null); }); // --------------------------------------------------------------------------- // 6. discoverRepoAgents / discoverFleetAgents // --------------------------------------------------------------------------- test("discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name", () => { const root = makeRoot(); const repoRoot = join(root, "repo"); mkdirSync(join(repoRoot, ".pi", "state", "b", "sessions"), { recursive: true }); mkdirSync(join(repoRoot, ".pi", "state", "a", "sessions"), { recursive: true }); mkdirSync(join(repoRoot, ".pi", "state", "c"), { recursive: true }); // no sessions dir: must be skipped const specs = discoverRepoAgents(repoRoot); assert.deepEqual(specs.map((s) => s.agent), ["a", "b"]); const project = basename(resolve(repoRoot)); for (const spec of specs) { assert.equal(spec.project, project); assert.equal(spec.sessionsDir, join(repoRoot, ".pi", "state", spec.agent, "sessions")); assert.deepEqual(spec.tmux, { socket: null, session: spec.agent }); } }); test("discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields", () => { const root = makeRoot(); const fleetRoot = join(root, "fleet"); mkdirSync(join(fleetRoot, "y", ".pi", "agent", "sessions"), { recursive: true }); mkdirSync(join(fleetRoot, "x", ".pi", "agent", "sessions"), { recursive: true }); mkdirSync(join(fleetRoot, "z", ".pi"), { recursive: true }); // no sessions dir: must be skipped const specs = discoverFleetAgents(fleetRoot); assert.deepEqual(specs.map((s) => s.agent), ["x", "y"]); for (const spec of specs) { assert.equal(spec.project, "fleet"); assert.equal(spec.sessionsDir, join(fleetRoot, spec.agent, ".pi", "agent", "sessions")); assert.deepEqual(spec.tmux, { socket: "mosaic-fleet", session: spec.agent }); } }); // --------------------------------------------------------------------------- // 7. scan // --------------------------------------------------------------------------- test("scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files", () => { const root = makeRoot(); const boardDir = join(root, "board"); const sessionsDir = join(root, "src-sessions"); mkdirSync(sessionsDir, { recursive: true }); const sessionPath = join(sessionsDir, "s.jsonl"); writeFile( sessionPath, [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done"] }), ].join("\n") + "\n" ); const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }]; const opts = { boardDir, isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z") }; const index1 = scan(specs, opts); const agentFile = join(boardDir, "sessions", "proj", "agent1.json"); const indexFile = join(boardDir, "index.json"); assert.ok(existsSync(agentFile)); assert.ok(existsSync(indexFile)); const rec1 = JSON.parse(readFileSync(agentFile, "utf8")); assert.equal(rec1.state, "waiting"); assert.equal(index1.counts.waiting, 1); assert.equal(index1.counts.working, 0); assert.deepEqual(index1.waitingOnYou, ["proj/agent1"]); assert.equal(index1.generatedAt, "2026-09-01T00:01:00.000Z"); // Flip the fixture to a "working" state and rescan; files must reflect the new state. writeFile( sessionPath, [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: "2026-09-01T00:02:00Z", role: "assistant", stopReason: "toolUse", texts: ["thinking"] }), ].join("\n") + "\n" ); const index2 = scan(specs, { ...opts, now: () => new Date("2026-09-01T00:03:00Z") }); const rec2 = JSON.parse(readFileSync(agentFile, "utf8")); assert.equal(rec2.state, "working"); assert.equal(index2.counts.working, 1); assert.equal(index2.counts.waiting, 0); assert.deepEqual(index2.waitingOnYou, []); // No leftover *.tmp-* artifacts anywhere under boardDir. const walk = (dir) => { const names = readdirSync(dir, { withFileTypes: true }); for (const entry of names) { const p = join(dir, entry.name); assert.ok(!entry.name.includes(".tmp-"), `leftover tmp file: ${p}`); if (entry.isDirectory()) walk(p); } }; walk(boardDir); }); test("scan: relative boardDir throws ConfigError", () => { assert.throws(() => scan([], { boardDir: "relative/board", isAlive: () => true }), ConfigError); }); // --------------------------------------------------------------------------- // 8. CLI // --------------------------------------------------------------------------- function runCli(args) { return spawnSync(process.execPath, [cli, ...args], { encoding: "utf8", timeout: 15000 }); } test("CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files", () => { 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 }); writeFile( join(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"] }), ].join("\n") + "\n" ); const r = runCli(["scan", "--config", configPath, "--repo", repoRoot, "--fleet", "none", "--liveness", "assume-alive", "--print"]); assert.equal(r.status, 0, r.stderr); assert.match(r.stdout, /^board: /m); const boardDir = join(dataRoot, "board"); assert.ok(existsSync(join(boardDir, "index.json"))); assert.ok(existsSync(join(boardDir, "sessions", "repo", "agent1.json"))); const index = JSON.parse(readFileSync(join(boardDir, "index.json"), "utf8")); assert.equal(index.sessions.length, 1); assert.equal(index.counts.waiting, 1); }); test("CLI: missing config exits 2 with a refused: message", () => { const root = makeRoot(); const r = runCli(["scan", "--config", join(root, "no-such-config.json")]); assert.equal(r.status, 2); assert.match(r.stderr, /^refused:/); }); test("CLI: unknown command exits 2", () => { const r = runCli(["bogus"]); assert.equal(r.status, 2); assert.match(r.stderr, /^refused:/); }); test("CLI: unknown --liveness value exits 2", () => { const r = runCli(["scan", "--liveness", "bogus"]); assert.equal(r.status, 2); assert.match(r.stderr, /^refused:/); }); // --------------------------------------------------------------------------- // 9. panesRunPi / tmuxIsAlive: liveness means a pane actually runs pi // --------------------------------------------------------------------------- test("panesRunPi: true when any trimmed line equals 'pi'", () => { assert.equal(panesRunPi("bash\npi\n"), true); assert.equal(panesRunPi("pi"), true); assert.equal(panesRunPi(" pi \n"), true); }); test("panesRunPi: false for bash-only, claude, empty, or node-pi-style lines", () => { assert.equal(panesRunPi("bash"), false); assert.equal(panesRunPi("claude"), false); assert.equal(panesRunPi(""), false); assert.equal(panesRunPi("node pi"), false); }); function fakeExec(result) { const calls = []; const exec = (cmd, args, opts) => { calls.push({ cmd, args, opts }); return result; }; exec.calls = calls; return exec; } test("tmuxIsAlive: a pane running pi is alive", () => { const exec = fakeExec({ status: 0, stdout: "bash\npi\n" }); assert.equal(tmuxIsAlive({ socket: null, session: "a" }, { exec }), true); }); test("tmuxIsAlive: session exists but pi has exited is not alive", () => { const exec = fakeExec({ status: 0, stdout: "bash\n" }); assert.equal(tmuxIsAlive({ socket: null, session: "a" }, { exec }), false); }); test("tmuxIsAlive: no such tmux session is not alive", () => { const exec = fakeExec({ status: 1, stdout: "" }); assert.equal(tmuxIsAlive({ socket: null, session: "a" }, { exec }), false); }); test("tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive", () => { const exec = fakeExec({ error: new Error("ENOENT") }); assert.equal(tmuxIsAlive({ socket: null, session: "a" }, { exec }), null); }); test("tmuxIsAlive: passes -L only when a socket is given", () => { const withSocket = fakeExec({ status: 0, stdout: "pi\n" }); tmuxIsAlive({ socket: "mosaic-fleet", session: "name" }, { exec: withSocket }); assert.equal(withSocket.calls[0].cmd, "tmux"); assert.deepEqual(withSocket.calls[0].args, ["-L", "mosaic-fleet", "list-panes", "-s", "-t", "=name", "-F", "#{pane_current_command}\t#{pane_current_path}"]); const noSocket = fakeExec({ status: 0, stdout: "pi\n" }); tmuxIsAlive({ socket: null, session: "name" }, { exec: noSocket }); assert.deepEqual(noSocket.calls[0].args, ["list-panes", "-s", "-t", "=name", "-F", "#{pane_current_command}\t#{pane_current_path}"]); }); test("parsePanes: one pane per line, command and optional tab-separated path", () => { assert.deepEqual(parsePanes("bash\t/home/x\npi\t/mnt/repo\n"), [ { command: "bash", path: "/home/x" }, { command: "pi", path: "/mnt/repo" }, ]); assert.deepEqual(parsePanes("pi\n"), [{ command: "pi", path: null }]); assert.deepEqual(parsePanes(""), []); }); test("tmuxInspect: reports the path of the pane running pi, not of a shell pane", () => { const exec = fakeExec({ status: 0, stdout: "bash\t/home/x\npi\t/mnt/repo/sub\n" }); assert.deepEqual(tmuxInspect({ socket: "mosaic-fleet", session: "a" }, { exec }), { alive: true, workspace: "/mnt/repo/sub" }); assert.deepEqual(exec.calls[0].args, ["-L", "mosaic-fleet", "list-panes", "-s", "-t", "=a", "-F", "#{pane_current_command}\t#{pane_current_path}"]); }); test("tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness", () => { assert.deepEqual(tmuxInspect({ socket: null, session: "a" }, { exec: fakeExec({ status: 0, stdout: "bash\t/home/x\n" }) }), { alive: false, workspace: null }); assert.deepEqual(tmuxInspect({ socket: null, session: "a" }, { exec: fakeExec({ status: 1, stdout: "" }) }), { alive: false, workspace: null }); assert.deepEqual(tmuxInspect({ socket: null, session: "a" }, { exec: fakeExec({ error: new Error("ENOENT") }) }), { alive: null, workspace: null }); }); // --------------------------------------------------------------------------- // 10. Seen marks: loadSeen / markSeen // --------------------------------------------------------------------------- test("loadSeen: missing file returns {}", () => { const root = makeRoot(); assert.deepEqual(loadSeen(root), {}); }); test("loadSeen: invalid JSON throws ConfigError", () => { const root = makeRoot(); writeFile(join(root, "seen.json"), "{ not json"); assert.throws(() => loadSeen(root), ConfigError); }); test("loadSeen: a JSON array throws ConfigError", () => { const root = makeRoot(); writeFile(join(root, "seen.json"), "[]"); assert.throws(() => loadSeen(root), ConfigError); }); test("loadSeen: a non-string value throws ConfigError", () => { const root = makeRoot(); writeFile(join(root, "seen.json"), JSON.stringify({ "p/a": 123 })); assert.throws(() => loadSeen(root), ConfigError); }); test("markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files", () => { const root = makeRoot(); const boardDir = join(root, "board"); const marks = markSeen(boardDir, { project: "p", agent: "a", lastActivity: "2026-09-01T00:00:00Z", seen: true }); assert.deepEqual(marks, { "p/a": "2026-09-01T00:00:00Z" }); const path = join(boardDir, "seen.json"); assert.ok(existsSync(path)); assert.equal(statSync(path).mode & 0o777, 0o600); const names = readdirSync(boardDir); assert.ok(!names.some((n) => n.includes(".tmp-")), `leftover tmp file among: ${names.join(", ")}`); }); test("markSeen: seen false deletes the key", () => { const root = makeRoot(); const boardDir = join(root, "board"); markSeen(boardDir, { project: "p", agent: "a", lastActivity: "t1", seen: true }); const marks = markSeen(boardDir, { project: "p", agent: "a", lastActivity: "t1", seen: false }); assert.deepEqual(marks, {}); }); test("markSeen: missing, empty, or non-string fields throw ConfigError", () => { const root = makeRoot(); const boardDir = join(root, "board"); assert.throws(() => markSeen(boardDir, { project: "p", agent: "a", lastActivity: "" }), ConfigError); assert.throws(() => markSeen(boardDir, { project: "p", agent: "a" }), ConfigError); assert.throws(() => markSeen(boardDir, { project: "p", agent: 5, lastActivity: "t" }), ConfigError); assert.throws(() => markSeen(boardDir, { agent: "a", lastActivity: "t" }), ConfigError); }); test("markSeen: project containing '/' throws ConfigError", () => { const root = makeRoot(); const boardDir = join(root, "board"); assert.throws(() => markSeen(boardDir, { project: "p/x", agent: "a", lastActivity: "t" }), ConfigError); }); test("markSeen: non-boolean seen throws ConfigError", () => { const root = makeRoot(); const boardDir = join(root, "board"); assert.throws(() => markSeen(boardDir, { project: "p", agent: "a", lastActivity: "t", seen: "true" }), ConfigError); }); // --------------------------------------------------------------------------- // 11. scanAgent: seen marks apply only to waiting/error, and only while the // mark's timestamp still matches the agent's newest message // --------------------------------------------------------------------------- test("scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); 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 seen = { "p/a": "2026-09-01T00:00:01Z" }; const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z"), seen }); assert.equal(rec.state, "waiting"); assert.equal(rec.seen, true); assert.equal(rec.waitingOnYou, false); }); test("scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); 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 seen = { "p/a": "2026-09-01T00:00:00Z" }; // stale: older than lastTimestamp const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z"), seen }); assert.equal(rec.state, "waiting"); assert.equal(rec.seen, false); assert.equal(rec.waitingOnYou, true); }); test("scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error)", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); 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: "toolUse", texts: ["thinking"] }), ]); const seen = { "p/a": "2026-09-01T00:00:01Z" }; const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z"), seen }); assert.equal(rec.state, "working"); assert.equal(rec.seen, false); assert.equal(rec.waitingOnYou, false); }); test("scanAgent: an error-state session with a matching mark is seen", () => { const root = makeRoot(); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); 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: "aborted" }), ]); const seen = { "p/a": "2026-09-01T00:00:01Z" }; const rec = scanAgent({ agent: "a", project: "p", sessionsDir, tmux: {} }, { isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z"), seen }); assert.equal(rec.state, "error"); assert.equal(rec.seen, true); assert.equal(rec.waitingOnYou, false); }); // --------------------------------------------------------------------------- // 12. scan: reads seen.json, index reflects it, and scanning never rewrites it // --------------------------------------------------------------------------- test("scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes", () => { const root = makeRoot(); const boardDir = join(root, "board"); const sessionsDir = join(root, "sessions"); mkdirSync(sessionsDir, { recursive: true }); const lastActivity = "2026-09-01T00:00:01Z"; writeSessionFile(sessionsDir, "s.jsonl", [ sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }), messageLine({ timestamp: lastActivity, role: "assistant", stopReason: "stop", texts: ["done"] }), ]); mkdirSync(boardDir, { recursive: true }); const seenPath = join(boardDir, "seen.json"); writeFile(seenPath, JSON.stringify({ "p/agent1": lastActivity })); const before = readFileSync(seenPath); const index = scan([{ agent: "agent1", project: "p", sessionsDir, tmux: {} }], { boardDir, isAlive: () => true, now: () => new Date("2026-09-01T00:01:00Z") }); assert.deepEqual(index.seen, ["p/agent1"]); assert.deepEqual(index.waitingOnYou, []); const after = readFileSync(seenPath); assert.deepEqual(before, after, "scan must never rewrite seen.json"); }); test("scan: a corrupt seen.json makes scan throw ConfigError (fail closed)", () => { const root = makeRoot(); const boardDir = join(root, "board"); mkdirSync(boardDir, { recursive: true }); writeFile(join(boardDir, "seen.json"), "{ bad json"); assert.throws(() => scan([], { boardDir, isAlive: () => true }), ConfigError); });