One cumulative control-board, webui and seat state. The four rows edit the
same files (scan.mjs, page.html, README.md, app.js), so they land together,
each on its own receipt:
- Row 18, Discord connector rows on the board (#1509): R3 approved by
Darkwing and Dewey, Gitea comment 26257, manifest 254403b8. Jason
accepted the visual test.
- Row 22, board attention status (#1503): Filbert approved R1, comment
26248, manifest e40b58ec; restart receipt 26249.
- #1511, task attribution (row 6 code phase): R2 approved by Filbert and
Dewey, manifest d4c96395. docs/TOOLS.md carries the approved --by usage
line (tools-usage.patch 86bcba3c).
- #1512, relaunch activity (row 6 pilot): R1 approved by Darkwing and
Dewey, candidate manifest 47769fad. All seven source files match it.
Row 16, internal development bootstrap (#1510): the seven files outside
shared records match Filbert's R1 pins, receipt 26204 (agents/researcher/*,
scripts/test-darkwing-launch.mjs, the bootstrap plan).
packages/webui/src/public/app.js is committed at its #1512 R1 pin ce7d79a4.
The working copy holds Dewey's unreviewed return-flow candidate on top of
that, and it stays uncommitted.
Also: the four row briefs and Darkwing's evidence records under
agents/darkwing/work, including the 2026-09-26 tree manifest and the #1512
re-run against 21e3e908. Serial acceptance command: 397/397, three runs.
The failures that only show when tests run concurrently are in #1509 engine
tests, and they reproduce on clean HEAD.
Suites on the exact staged tree: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63; package union 397/397
(serial); test-darkwing-launch 5/5.
Shared records (BUILD-LOG, QUEUE, CURRENT, DEFERRED, SESSIONS, AGENTS.md,
agents/README.md) follow in Sage's records commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
734 lines
32 KiB
JavaScript
734 lines
32 KiB
JavaScript
import { test, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
chmodSync,
|
|
symlinkSync,
|
|
statSync,
|
|
existsSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve, dirname, basename } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
SeatError,
|
|
loadDataRoot,
|
|
seatsDir,
|
|
registrationPath,
|
|
resolveSeat,
|
|
tmuxContext,
|
|
validateRegistration,
|
|
makeRegistration,
|
|
writeRegistration,
|
|
readRegistration,
|
|
updateTask,
|
|
findRegistrations,
|
|
samePath,
|
|
resolveSetBy,
|
|
TASK_LIMIT,
|
|
SET_BY_NAME,
|
|
SET_BY_UNKNOWN,
|
|
} from "../src/seat.mjs";
|
|
|
|
const pkgRoot = resolve(import.meta.dirname, "..");
|
|
const cli = join(pkgRoot, "src", "cli.mjs");
|
|
|
|
// Track every tmpdir so a stray failure never leaves fixtures behind.
|
|
const roots = [];
|
|
function makeRoot() {
|
|
const root = mkdtempSync(join(tmpdir(), "seat-test-"));
|
|
roots.push(root);
|
|
return root;
|
|
}
|
|
after(() => {
|
|
for (const root of roots) rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
function writeFile(path, content) {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, content);
|
|
}
|
|
|
|
function writeExecutable(path, content) {
|
|
writeFile(path, content);
|
|
chmodSync(path, 0o755);
|
|
}
|
|
|
|
const FAKE_LAUNCH_SH = [
|
|
"#!/usr/bin/env bash",
|
|
'printf \'%s\\n\' "$@" > "$(dirname "$0")/args.txt"',
|
|
'printf \'%s\\n\' "${MOSAIC_LAUNCH_REGISTERED:-unset}" > "$(dirname "$0")/env.txt"',
|
|
'exit "${FAKE_EXIT:-0}"',
|
|
"",
|
|
].join("\n");
|
|
|
|
// Repo layout fixture: <root>/.git, <root>/agents/<seat>/launch.sh.
|
|
function buildRepoSeat(root, seat, { launchContent = FAKE_LAUNCH_SH, executable = true } = {}) {
|
|
mkdirSync(join(root, ".git"), { recursive: true });
|
|
const seatDir = join(root, "agents", seat);
|
|
const launchScript = join(seatDir, "launch.sh");
|
|
if (executable) writeExecutable(launchScript, launchContent);
|
|
else writeFile(launchScript, launchContent);
|
|
return seatDir;
|
|
}
|
|
|
|
// Fleet layout fixture: <root>/fleet/agents/<seat>/{launch.sh,.pi/}, no <root>/.git.
|
|
function buildFleetSeat(root, seat) {
|
|
const seatDir = join(root, "fleet", "agents", seat);
|
|
writeExecutable(join(seatDir, "launch.sh"), FAKE_LAUNCH_SH);
|
|
mkdirSync(join(seatDir, ".pi"), { recursive: true });
|
|
return seatDir;
|
|
}
|
|
|
|
function buildConfig(root, dataRoot = join(root, "data")) {
|
|
const configPath = join(root, "config.json");
|
|
writeFile(configPath, JSON.stringify({ configVersion: 1, dataRoot }));
|
|
return { configPath, dataRoot };
|
|
}
|
|
|
|
function cliEnv(overrides = {}) {
|
|
const env = { ...process.env, TMUX: "", TMUX_PANE: "" };
|
|
delete env.MOSAIC_LAUNCH_REGISTERED;
|
|
// The developer's shell may carry a seat name (agents/rocko/launch.sh
|
|
// exports one); attribution tests set it explicitly or not at all.
|
|
delete env.MOSAIC_AGENT_NAME;
|
|
return { ...env, ...overrides };
|
|
}
|
|
|
|
function runCli(args, { cwd, env = cliEnv() } = {}) {
|
|
return spawnSync(process.execPath, [cli, ...args], { cwd, encoding: "utf8", env, timeout: 15000 });
|
|
}
|
|
|
|
function baseResolved(seatDir, seat = "myseat") {
|
|
return {
|
|
seat,
|
|
seatDir,
|
|
launchScript: join(seatDir, "launch.sh"),
|
|
layout: "repo",
|
|
project: "someproject",
|
|
sessionsDir: join(seatDir, "sessions"),
|
|
defaultWorkspace: dirname(dirname(seatDir)),
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1-3. resolveSeat
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("resolveSeat: by name under --repo resolves the repo layout", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildRepoSeat(root, "myseat");
|
|
const resolved = resolveSeat("myseat", { repo: root });
|
|
assert.equal(resolved.seat, "myseat");
|
|
assert.equal(resolved.seatDir, seatDir);
|
|
assert.equal(resolved.layout, "repo");
|
|
assert.equal(resolved.project, basename(root));
|
|
assert.equal(resolved.sessionsDir, join(root, ".pi", "state", "myseat", "sessions"));
|
|
assert.equal(resolved.defaultWorkspace, root);
|
|
assert.equal(resolved.launchScript, join(seatDir, "launch.sh"));
|
|
});
|
|
|
|
test("resolveSeat: by path resolves the fleet layout", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildFleetSeat(root, "myseat");
|
|
const resolved = resolveSeat(seatDir);
|
|
assert.equal(resolved.seat, "myseat");
|
|
assert.equal(resolved.layout, "fleet");
|
|
assert.equal(resolved.project, null);
|
|
assert.equal(resolved.sessionsDir, join(seatDir, ".pi", "agent", "sessions"));
|
|
assert.equal(resolved.defaultWorkspace, null);
|
|
});
|
|
|
|
test("resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout", () => {
|
|
const root = makeRoot();
|
|
|
|
// Missing seat directory entirely.
|
|
assert.throws(() => resolveSeat(join(root, "agents", "ghost")), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 2);
|
|
return true;
|
|
});
|
|
|
|
// Seat directory exists but has no launch.sh.
|
|
const noLaunchDir = join(root, "agents", "nolaunch");
|
|
mkdirSync(noLaunchDir, { recursive: true });
|
|
assert.throws(() => resolveSeat(noLaunchDir), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 2);
|
|
return true;
|
|
});
|
|
|
|
// launch.sh present but not executable.
|
|
const notExecDir = join(root, "agents", "notexec");
|
|
writeFile(join(notExecDir, "launch.sh"), FAKE_LAUNCH_SH);
|
|
chmodSync(join(notExecDir, "launch.sh"), 0o644);
|
|
assert.throws(() => resolveSeat(notExecDir), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 2);
|
|
return true;
|
|
});
|
|
|
|
// Invalid seat name.
|
|
assert.throws(() => resolveSeat("Bad Name"), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 4);
|
|
return true;
|
|
});
|
|
|
|
// Parent not named "agents", no .git, no .pi: layout "unknown".
|
|
const looseDir = join(root, "loose", "myseat");
|
|
writeExecutable(join(looseDir, "launch.sh"), FAKE_LAUNCH_SH);
|
|
const resolved = resolveSeat(looseDir);
|
|
assert.equal(resolved.layout, "unknown");
|
|
assert.equal(resolved.project, null);
|
|
assert.equal(resolved.sessionsDir, null);
|
|
assert.equal(resolved.defaultWorkspace, null);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. tmuxContext
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("tmuxContext: outside tmux, default socket, custom socket, and exec failure", () => {
|
|
// No TMUX var at all.
|
|
assert.equal(tmuxContext({ env: {} }), null);
|
|
// Empty TMUX (matches TMUX: "" in CLI fixtures).
|
|
assert.equal(tmuxContext({ env: { TMUX: "" } }), null);
|
|
|
|
// Default socket, with TMUX_PANE set: session name comes from exec stdout.
|
|
let captured = null;
|
|
const okExec = (cmd, args) => {
|
|
captured = { cmd, args };
|
|
return { status: 0, stdout: "sess\n" };
|
|
};
|
|
const result = tmuxContext({
|
|
env: { TMUX: "/tmp/tmux-1000/default,123,0", TMUX_PANE: "%3" },
|
|
exec: okExec,
|
|
});
|
|
assert.deepEqual(result, { socket: null, session: "sess" });
|
|
assert.equal(captured.cmd, "tmux");
|
|
assert.deepEqual(captured.args.slice(0, 4), ["-S", "/tmp/tmux-1000/default", "display-message", "-p"]);
|
|
assert.ok(captured.args.includes("-t"));
|
|
assert.equal(captured.args[captured.args.indexOf("-t") + 1], "%3");
|
|
|
|
// Non-default socket name is surfaced.
|
|
const namedSocket = tmuxContext({
|
|
env: { TMUX: "/tmp/tmux-1000/mosaic-fleet,1,0" },
|
|
exec: () => ({ status: 0, stdout: "other\n" }),
|
|
});
|
|
assert.deepEqual(namedSocket, { socket: "mosaic-fleet", session: "other" });
|
|
|
|
// Non-zero exit and exec error both yield null.
|
|
assert.equal(tmuxContext({ env: { TMUX: "/tmp/tmux-1000/default,1,0" }, exec: () => ({ status: 1, stdout: "" }) }), null);
|
|
assert.equal(
|
|
tmuxContext({ env: { TMUX: "/tmp/tmux-1000/default,1,0" }, exec: () => ({ error: new Error("no tmux"), status: null }) }),
|
|
null
|
|
);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. makeRegistration + validateRegistration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("makeRegistration produces a record that validates; each shape violation throws SeatError", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildRepoSeat(root, "myseat");
|
|
const resolved = resolveSeat("myseat", { repo: root });
|
|
const record = makeRegistration({ resolved, task: "do things", pid: 1234 });
|
|
assert.deepEqual(validateRegistration(record), record);
|
|
|
|
const mutate = (patch) => ({ ...record, ...patch });
|
|
|
|
assert.throws(() => validateRegistration(mutate({ bogusField: "x" })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ version: 2 })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ task: "x".repeat(TASK_LIMIT + 1) })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ pid: 1.5 })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ pid: "1234" })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ tmux: { socket: 5, session: "s" } })), SeatError);
|
|
assert.throws(() => validateRegistration(mutate({ tmux: { socket: null } })), SeatError);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. writeRegistration / readRegistration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("writeRegistration/readRegistration: round trip, permissions, absence, and malformed records", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildRepoSeat(root, "myseat");
|
|
const resolved = resolveSeat("myseat", { repo: root });
|
|
const seats = seatsDir(join(root, "data"));
|
|
const record = makeRegistration({ resolved, task: "hello", pid: 42 });
|
|
|
|
const path = writeRegistration(seats, record);
|
|
assert.deepEqual(readRegistration(seats, "myseat", "repo"), record);
|
|
|
|
assert.equal(statSync(path).mode & 0o777, 0o600);
|
|
assert.equal(statSync(dirname(path)).mode & 0o777, 0o700);
|
|
|
|
// No leftover *.tmp-* artifacts after the atomic write.
|
|
for (const name of readdirSync(dirname(path))) assert.ok(!name.includes(".tmp-"), `leftover tmp file: ${name}`);
|
|
|
|
// Missing registration reads as null.
|
|
assert.equal(readRegistration(seats, "ghost", "repo"), null);
|
|
|
|
// Malformed JSON throws.
|
|
const malformedSeat = "malformed";
|
|
writeFile(registrationPath(seats, malformedSeat, "repo"), "{ not json");
|
|
assert.throws(() => readRegistration(seats, malformedSeat, "repo"), SeatError);
|
|
|
|
// A record with an unknown field throws rather than reading as absent.
|
|
const unknownFieldSeat = "unknownfield";
|
|
const badRecord = { ...record, seat: unknownFieldSeat, extra: "nope" };
|
|
writeFile(registrationPath(seats, unknownFieldSeat, "repo"), JSON.stringify(badRecord));
|
|
assert.throws(() => readRegistration(seats, unknownFieldSeat, "repo"), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.match(err.message, /unknown field/);
|
|
return true;
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. updateTask
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildRepoSeat(root, "myseat");
|
|
const resolved = resolveSeat("myseat", { repo: root });
|
|
const seats = seatsDir(join(root, "data"));
|
|
const record = makeRegistration({ resolved, task: "original", pid: 1 });
|
|
writeRegistration(seats, record);
|
|
|
|
const updated = updateTask(seats, "myseat", "revised");
|
|
assert.equal(updated.task, "revised");
|
|
assert.equal(updated.taskSetBy, SET_BY_UNKNOWN, "no setBy option records the literal unknown");
|
|
assert.notEqual(updated.updatedAt, null);
|
|
for (const key of Object.keys(record)) {
|
|
if (key === "task" || key === "updatedAt") continue;
|
|
assert.deepEqual(updated[key], record[key], `field ${key} changed unexpectedly`);
|
|
}
|
|
assert.equal(updated.startedAt, record.startedAt, "startedAt is the launch time and survives a task update");
|
|
assert.deepEqual(Object.keys(updated).sort(), [...Object.keys(record), "taskSetBy"].sort(), "exactly one field is added");
|
|
|
|
// Refuses when there is no registration to attach to.
|
|
assert.throws(() => updateTask(seats, "ghost", "x"), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 1);
|
|
return true;
|
|
});
|
|
|
|
// Refuses an over-limit task.
|
|
assert.throws(() => updateTask(seats, "myseat", "x".repeat(TASK_LIMIT + 1)), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 4);
|
|
return true;
|
|
});
|
|
|
|
// The same name in a second layout makes the bare name ambiguous; the
|
|
// layout qualifier resolves it and each layout keeps its own record.
|
|
const fleetRecord = { ...record, layout: "fleet", sessionsDir: join(root, "fleet", "myseat", ".pi", "agent", "sessions"), task: "fleet task" };
|
|
writeRegistration(seats, fleetRecord);
|
|
assert.throws(() => updateTask(seats, "myseat", "x"), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 4);
|
|
assert.match(err.message, /more than one layout/);
|
|
return true;
|
|
});
|
|
assert.equal(updateTask(seats, "myseat", "fleet revised", { layout: "fleet" }).task, "fleet revised");
|
|
assert.equal(readRegistration(seats, "myseat", "repo").task, "revised");
|
|
assert.equal(readRegistration(seats, "myseat", "fleet").task, "fleet revised");
|
|
assert.equal(findRegistrations(seats, "myseat").length, 2);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. CLI launch end to end
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("CLI launch: registers, execs the fake launch script, and passes args through", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
|
|
const r = runCli(["launch", "myseat", "--task", "do x", "--config", configPath, "--", "--foo", "bar"], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
const seatDir = join(root, "agents", "myseat");
|
|
assert.equal(readFileSync(join(seatDir, "args.txt"), "utf8"), "--foo\nbar\n");
|
|
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
assert.equal(readFileSync(join(seatDir, "env.txt"), "utf8").trim(), path);
|
|
|
|
const record = JSON.parse(readFileSync(path, "utf8"));
|
|
assert.equal(record.seat, "myseat");
|
|
assert.equal(record.project, basename(root));
|
|
assert.equal(record.task, "do x");
|
|
assert.equal(record.workspace, root);
|
|
assert.equal(record.harness, null);
|
|
assert.equal(record.tmux, null);
|
|
assert.ok(Number.isInteger(record.pid) && record.pid > 0);
|
|
assert.equal(record.layout, "repo");
|
|
assert.ok(Number.isFinite(Date.parse(record.startedAt)));
|
|
});
|
|
|
|
test("CLI launch: --harness lands in the record", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
|
|
const r = runCli(["launch", "myseat", "--task", "t", "--harness", "pi", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
const record = JSON.parse(readFileSync(path, "utf8"));
|
|
assert.equal(record.harness, "pi");
|
|
});
|
|
|
|
test("CLI launch: the launch script's own exit code passes through", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath } = buildConfig(root);
|
|
|
|
const r = runCli(["launch", "myseat", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath, FAKE_EXIT: "7" }),
|
|
});
|
|
assert.equal(r.status, 7, r.stderr);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 9. CLI relaunch rewrites the single registration file
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("CLI launch: relaunching a seat rewrites the one registration record", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
|
|
const r1 = runCli(["launch", "myseat", "--task", "first", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r1.status, 0, r1.stderr);
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
const record1 = JSON.parse(readFileSync(path, "utf8"));
|
|
|
|
const r2 = runCli(["launch", "myseat", "--task", "second", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r2.status, 0, r2.stderr);
|
|
const record2 = JSON.parse(readFileSync(path, "utf8"));
|
|
|
|
assert.deepEqual(readdirSync(dirname(path)), ["registration.json"]);
|
|
assert.equal(record2.task, "second");
|
|
assert.ok(Date.parse(record2.startedAt) >= Date.parse(record1.startedAt));
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 10. CLI launch with no --task
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("CLI launch: omitting --task records an empty string, not null", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
|
|
const r = runCli(["launch", "myseat", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
const record = JSON.parse(readFileSync(path, "utf8"));
|
|
assert.equal(record.task, "");
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 11. CLI seat task
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
|
|
const launchResult = runCli(["launch", "myseat", "--task", "before", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(launchResult.status, 0, launchResult.stderr);
|
|
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
const before = JSON.parse(readFileSync(path, "utf8"));
|
|
|
|
const r = runCli(["seat", "task", "myseat", "new text", "--config", configPath], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath }),
|
|
});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
|
|
const after1 = JSON.parse(readFileSync(path, "utf8"));
|
|
assert.equal(after1.task, "new text");
|
|
for (const key of Object.keys(before)) {
|
|
if (key === "task" || key === "updatedAt") continue;
|
|
assert.deepEqual(after1[key], before[key], `field ${key} changed unexpectedly`);
|
|
}
|
|
|
|
// No registration for a seat that was never launched.
|
|
const root2 = makeRoot();
|
|
buildRepoSeat(root2, "unlaunched");
|
|
const { configPath: configPath2 } = buildConfig(root2);
|
|
const r2 = runCli(["seat", "task", "unlaunched", "text", "--config", configPath2], {
|
|
cwd: root2,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath2 }),
|
|
});
|
|
assert.equal(r2.status, 1);
|
|
assert.match(r2.stderr, /no registration/);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 12. CLI refusals
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure", () => {
|
|
// No args: usage, exit 4.
|
|
const rNoArgs = runCli([]);
|
|
assert.equal(rNoArgs.status, 4);
|
|
assert.match(rNoArgs.stderr, /usage: mosaic launch/);
|
|
|
|
// Unknown flag: exit 4.
|
|
const rUnknown = runCli(["launch", "myseat", "--bogus"]);
|
|
assert.equal(rUnknown.status, 4);
|
|
assert.match(rUnknown.stderr, /unknown argument/);
|
|
|
|
// Missing config with an otherwise-valid seat: exit 2.
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const missingConfig = join(root, "missing.json");
|
|
assert.ok(!existsSync(missingConfig));
|
|
const rMissingConfig = runCli(["launch", "myseat", "--config", missingConfig], {
|
|
cwd: root,
|
|
env: cliEnv({ MOSAIC_CONFIG: missingConfig }),
|
|
});
|
|
assert.equal(rMissingConfig.status, 2);
|
|
assert.match(rMissingConfig.stderr, /config not found/);
|
|
|
|
// MOSAIC_LAUNCH_REGISTERED already set: exit 4, no registration written.
|
|
const root2 = makeRoot();
|
|
buildRepoSeat(root2, "myseat");
|
|
const { configPath: configPath2, dataRoot: dataRoot2 } = buildConfig(root2);
|
|
const rRegistered = runCli(["launch", "myseat", "--config", configPath2], {
|
|
cwd: root2,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath2, MOSAIC_LAUNCH_REGISTERED: "/some/path" }),
|
|
});
|
|
assert.equal(rRegistered.status, 4);
|
|
assert.match(rRegistered.stderr, /MOSAIC_LAUNCH_REGISTERED/);
|
|
assert.ok(!existsSync(registrationPath(seatsDir(dataRoot2), "myseat", "repo")));
|
|
|
|
// Launch script that fails to exec: exit 1, no registration left behind.
|
|
const root3 = makeRoot();
|
|
buildRepoSeat(root3, "myseat", { launchContent: "#!/nonexistent/interp\necho hi\n" });
|
|
const { configPath: configPath3, dataRoot: dataRoot3 } = buildConfig(root3);
|
|
const rExecFail = runCli(["launch", "myseat", "--config", configPath3], {
|
|
cwd: root3,
|
|
env: cliEnv({ MOSAIC_CONFIG: configPath3 }),
|
|
});
|
|
assert.equal(rExecFail.status, 1);
|
|
assert.match(rExecFail.stderr, /could not run/);
|
|
assert.ok(!existsSync(registrationPath(seatsDir(dataRoot3), "myseat", "repo")));
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 13. samePath
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("samePath: equal paths, symlinked dirs, distinct dirs, and non-strings", () => {
|
|
const root = makeRoot();
|
|
const target = join(root, "target");
|
|
mkdirSync(target);
|
|
const other = join(root, "other");
|
|
mkdirSync(other);
|
|
const link = join(root, "link");
|
|
symlinkSync(target, link, "dir");
|
|
|
|
assert.equal(samePath(target, target), true);
|
|
assert.equal(samePath(target, link), true);
|
|
assert.equal(samePath(target, other), false);
|
|
assert.equal(samePath(1, "x"), false);
|
|
assert.equal(samePath(null, target), false);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 14. Task attribution (#1511): taskSetBy on the record, resolveSetBy, and the
|
|
// --by / MOSAIC_AGENT_NAME precedence at the CLI. Attribution is a claim by
|
|
// the caller: bounded, escaped by the presentations, never authority.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test("resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4", () => {
|
|
assert.equal(resolveSetBy({ by: "darkwing", env: { MOSAIC_AGENT_NAME: "rocko" } }), "darkwing");
|
|
assert.equal(resolveSetBy({ by: null, env: { MOSAIC_AGENT_NAME: "rocko" } }), "rocko");
|
|
assert.equal(resolveSetBy({ env: {} }), SET_BY_UNKNOWN);
|
|
assert.equal(resolveSetBy({ env: { MOSAIC_AGENT_NAME: "" } }), SET_BY_UNKNOWN);
|
|
assert.equal(resolveSetBy({ by: "jason@host:1", env: {} }), "jason@host:1");
|
|
assert.equal(resolveSetBy({ by: "a".repeat(64), env: {} }), "a".repeat(64), "64 characters is the bound");
|
|
const refused = (fn, pattern) => assert.throws(fn, (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.equal(err.exitCode, 4);
|
|
assert.match(err.message, pattern);
|
|
return true;
|
|
});
|
|
for (const bad of ["", " ", "a".repeat(65), "<b>x</b>", "two words", "-leading", ".leading", "tab\tname", "new\nline", "ünïcode", "a/b", "a;b", "$(x)"]) {
|
|
refused(() => resolveSetBy({ by: bad, env: { MOSAIC_AGENT_NAME: "valid" } }), /--by/);
|
|
assert.equal(SET_BY_NAME.test(bad), false);
|
|
// The offending value is never echoed back.
|
|
if (bad.trim().length > 1) assert.throws(() => resolveSetBy({ by: bad, env: {} }), (err) => !err.message.includes(bad));
|
|
}
|
|
refused(() => resolveSetBy({ by: 42, env: {} }), /--by/);
|
|
// A set but malformed environment value is refused, not read as unknown:
|
|
// silently reporting "unknown" would hide a misconfigured seat, and --by
|
|
// is the explicit way past it.
|
|
refused(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "<script>" } }), /MOSAIC_AGENT_NAME/);
|
|
refused(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "a".repeat(65) } }), /MOSAIC_AGENT_NAME/);
|
|
assert.throws(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "<script>" } }), (err) => !err.message.includes("<script>"));
|
|
assert.equal(resolveSetBy({ by: "explicit", env: { MOSAIC_AGENT_NAME: "<script>" } }), "explicit", "an explicit value does not consult the environment at all");
|
|
});
|
|
|
|
test("validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change", () => {
|
|
const root = makeRoot();
|
|
const seatDir = buildRepoSeat(root, "old");
|
|
const resolved = resolveSeat("old", { repo: root });
|
|
const seats = seatsDir(join(root, "data"));
|
|
const record = makeRegistration({ resolved, task: "launched task", pid: 1 });
|
|
assert.equal("taskSetBy" in record, false, "launch does not attribute; only seat task does");
|
|
assert.equal(record.version, 1);
|
|
// An old on-disk record, byte for byte, without the field.
|
|
const path = registrationPath(seats, "old", "repo");
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
const oldBytes = JSON.stringify(record, null, 2) + "\n";
|
|
writeFileSync(path, oldBytes);
|
|
const loaded = readRegistration(seats, "old", "repo");
|
|
assert.deepEqual(loaded, record);
|
|
assert.equal(loaded.taskSetBy, undefined);
|
|
assert.equal(readFileSync(path, "utf8"), oldBytes, "reading never rewrites (no implicit migration)");
|
|
assert.equal(findRegistrations(seats, "old").length, 1);
|
|
// Valid values.
|
|
for (const ok of ["unknown", "darkwing", "jason@host:1", "a".repeat(64)]) validateRegistration({ ...record, taskSetBy: ok });
|
|
// Invalid values, never echoed.
|
|
for (const bad of [null, 7, "", "<b>", "two words", "a".repeat(65), {}]) {
|
|
assert.throws(() => validateRegistration({ ...record, taskSetBy: bad }), (err) => {
|
|
assert.ok(err instanceof SeatError);
|
|
assert.match(err.message, /taskSetBy/);
|
|
if (typeof bad === "string" && bad.length > 1) assert.equal(err.message.includes(bad), false);
|
|
return true;
|
|
});
|
|
}
|
|
writeFileSync(path, JSON.stringify({ ...record, taskSetBy: "<b>" }));
|
|
assert.throws(() => readRegistration(seats, "old", "repo"), /taskSetBy/);
|
|
});
|
|
|
|
test("updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const resolved = resolveSeat("myseat", { repo: root });
|
|
const seats = seatsDir(join(root, "data"));
|
|
const started = new Date("2026-09-10T10:00:00Z");
|
|
const record = makeRegistration({ resolved, task: "original", harness: "pi", tmux: { socket: "mosaic-fleet", session: "myseat" }, pid: 1, workspace: "/w", project: "proj", now: () => started });
|
|
writeRegistration(seats, record);
|
|
|
|
const first = updateTask(seats, "myseat", "by darkwing", { setBy: "darkwing", now: () => new Date("2026-09-14T10:00:00Z") });
|
|
assert.equal(first.taskSetBy, "darkwing");
|
|
assert.equal(first.startedAt, started.toISOString());
|
|
assert.equal(first.updatedAt, "2026-09-14T10:00:00.000Z");
|
|
for (const key of ["seat", "project", "workspace", "tmux", "harness", "pid", "sessionsDir", "seatDir", "launchScript", "layout", "version"]) {
|
|
assert.deepEqual(first[key], record[key], `field ${key} changed unexpectedly`);
|
|
}
|
|
assert.deepEqual(readRegistration(seats, "myseat", "repo"), first);
|
|
|
|
const second = updateTask(seats, "myseat", "by jason", { setBy: "jason", now: () => new Date("2026-09-14T11:00:00Z") });
|
|
assert.equal(second.taskSetBy, "jason", "a later set replaces the attribution; it is not appended");
|
|
assert.equal(second.startedAt, started.toISOString());
|
|
|
|
const third = updateTask(seats, "myseat", "anonymous");
|
|
assert.equal(third.taskSetBy, SET_BY_UNKNOWN, "no setBy means unknown, never the previous name");
|
|
|
|
// Invalid setBy refuses with exit 4 before anything is written.
|
|
const before = readFileSync(registrationPath(seats, "myseat", "repo"), "utf8");
|
|
assert.throws(() => updateTask(seats, "myseat", "x", { setBy: "<b>" }), (err) => err instanceof SeatError && err.exitCode === 4);
|
|
assert.throws(() => updateTask(seats, "myseat", "x", { setBy: "" }), (err) => err instanceof SeatError && err.exitCode === 4);
|
|
assert.equal(readFileSync(registrationPath(seats, "myseat", "repo"), "utf8"), before);
|
|
});
|
|
|
|
test("CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte", () => {
|
|
const root = makeRoot();
|
|
buildRepoSeat(root, "myseat");
|
|
const { configPath, dataRoot } = buildConfig(root);
|
|
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
|
|
const launchResult = runCli(["launch", "myseat", "--task", "before", "--config", configPath], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath, MOSAIC_AGENT_NAME: "launcher" }) });
|
|
assert.equal(launchResult.status, 0, launchResult.stderr);
|
|
const launched = JSON.parse(readFileSync(path, "utf8"));
|
|
assert.equal("taskSetBy" in launched, false, "launch --task does not attribute, even with the variable set");
|
|
|
|
const run = (args, env) => runCli(["seat", "task", "myseat", ...args, "--config", configPath], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath, ...env }) });
|
|
const read = () => JSON.parse(readFileSync(path, "utf8"));
|
|
|
|
let r = run(["one", "--by", "darkwing"], { MOSAIC_AGENT_NAME: "rocko" });
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /set by darkwing/);
|
|
assert.equal(read().taskSetBy, "darkwing");
|
|
assert.equal(read().task, "one");
|
|
assert.equal(read().startedAt, launched.startedAt);
|
|
|
|
r = run(["two"], { MOSAIC_AGENT_NAME: "rocko" });
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /set by rocko/);
|
|
assert.equal(read().taskSetBy, "rocko");
|
|
|
|
r = run(["three"], {});
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /set by unknown/);
|
|
assert.equal(read().taskSetBy, "unknown");
|
|
|
|
r = run(["four"], { MOSAIC_AGENT_NAME: "" });
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.equal(read().taskSetBy, "unknown");
|
|
for (const key of Object.keys(launched)) {
|
|
if (key === "task" || key === "updatedAt") continue;
|
|
assert.deepEqual(read()[key], launched[key], `field ${key} changed unexpectedly`);
|
|
}
|
|
|
|
const frozen = readFileSync(path, "utf8");
|
|
r = run(["five", "--by", "<script>alert(1)</script>"], { MOSAIC_AGENT_NAME: "rocko" });
|
|
assert.equal(r.status, 4);
|
|
assert.match(r.stderr, /refused: --by/);
|
|
assert.equal(r.stderr.includes("<script>"), false, "the refusal does not echo the value");
|
|
assert.equal(readFileSync(path, "utf8"), frozen, "an invalid --by writes nothing");
|
|
r = run(["five", "--by", ""], {});
|
|
assert.equal(r.status, 4);
|
|
assert.equal(readFileSync(path, "utf8"), frozen);
|
|
r = runCli(["seat", "task", "myseat", "five", "--config", configPath, "--by"], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath }) });
|
|
assert.equal(r.status, 4);
|
|
assert.match(r.stderr, /missing value for --by/);
|
|
assert.equal(readFileSync(path, "utf8"), frozen);
|
|
r = run(["five"], { MOSAIC_AGENT_NAME: "not a name" });
|
|
assert.equal(r.status, 4);
|
|
assert.match(r.stderr, /MOSAIC_AGENT_NAME/);
|
|
assert.equal(r.stderr.includes("not a name"), false);
|
|
assert.equal(readFileSync(path, "utf8"), frozen, "an invalid environment value writes nothing");
|
|
r = run(["six", "--by", "explicit"], { MOSAIC_AGENT_NAME: "not a name" });
|
|
assert.equal(r.status, 0, "an explicit --by is the way past a bad environment value");
|
|
assert.equal(read().taskSetBy, "explicit");
|
|
assert.equal(read().task, "six");
|
|
});
|