feat(cli): the mosaic CLI, broker host and decision notifier (row 39, S4, rocko)

packages/cli adds mosaic inbox, decide, tasks, agents and trail over the
human-cli transport, and mosaic bus start, stop and status as the trusted
host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots
packages/bus/src/process.mjs, passes config.trackers from the tracker.*
variables (lead decision 70), and runs a notifier child. The notifier DMs
each open blocking decision once and sends an 08:00 America/Chicago
digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord
ids. A torn journal tail is copied aside and truncated; a malformed line,
a directory looser than 0700 or a symlinked journal refuses (lead
decision 71). packages/discord gains dmRecipient, createDm and notify.mjs.

Candidate agents/rocko/work/slice1-s4, base b9b6cf00, build.patch
b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on
#1521 (comment 26855), Filbert approved round 2 (comment 26856). The
packet's mutant table lists M28 as killed; it survived, and BUILD-LOG
records the correction.

Integration gate in a worktree on 2557e29d with the patch applied:
bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail
on the base; S4 doesn't touch the package. Every scripts/test-*.sh is
green, with test-release 14/14 and test-task 98/98 on the existing gate2
compose network. A scratch test, not in this commit, booted the real
host with trackers against S3's fake Vikunja: the adapter went ready and
a task.close on a missing task answered task-not-found after a Vikunja
read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-09 07:49:38 -05:00
co-authored by Claude Opus 5.5
parent 2557e29dc7
commit 2f5303c1c7
29 changed files with 2610 additions and 6 deletions
+187
View File
@@ -0,0 +1,187 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { main, readNotifyConfig } from "../src/cli.mjs";
import { CliError } from "../src/errors.mjs";
import { hostDir, hostFile, startTimeOf } from "../src/host.mjs";
import { broker, io, notifyConfig, tmp } from "./helpers.mjs";
import { writeJson } from "../../business/tests/helpers.mjs";
import { join } from "node:path";
const exitOf = async (p) => {
try {
await p;
return 0;
} catch (e) {
if (!(e instanceof CliError)) throw e;
return e.exitCode;
}
};
function setup(t) {
const bus = broker(t);
const dataRoot = tmp(t);
const run = async (argv, x = io()) => ({ code: await exitOf(main(argv, x, { system: { dataRoot }, transport: bus.transport })), io: x });
return { ...bus, dataRoot, run };
}
test("inbox lists only decisions routed to the human, with what approving authorizes and how to decide", async (t) => {
const s = setup(t);
s.raise("task.scope.change", { domain: "technical", target: "task-1" });
const gated = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
const { code, io: x } = await s.run(["inbox", "--business", "demo"]);
assert.equal(code, 0);
const text = x.out.text;
assert.match(text, /^inbox: 1 open decision\(s\)/);
assert.match(text, new RegExp(`${gated.id.slice(0, 8)} git\\.push\\.protected \\(gated, blocking\\)`));
assert.match(text, /action: git\.push\.protected on refactor/);
assert.match(text, /choosing "yes" authorizes it; any other choice declines/);
assert.match(text, /task: vikunja:1\/7/);
assert.match(text, new RegExp(`decide: mosaic decide ${gated.id.slice(0, 8)} <option>`));
const json = await s.run(["inbox", "--business", "demo", "--json"]);
assert.equal(JSON.parse(json.io.out.text)[0].id, gated.id);
assert.deepEqual(s.calls.map((c) => c.verb), ["inbox", "inbox"]);
});
test("decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
const { code, io: x } = await s.run(["decide", d.id.slice(0, 8), "yes", "--yes", "--note", "ship it", "--business", "demo"]);
assert.equal(code, 0, x.err.text);
assert.match(x.out.text, /your choice: yes \(Allow\); this authorizes the action/);
assert.match(x.out.text, new RegExp(`resolved ${d.id}: yes`));
assert.deepEqual(s.calls.at(-1), { business: "demo", verb: "decision.resolve", args: { id: d.id, choice: "yes", note: "ship it" } });
assert.deepEqual(s.read("inbox"), []);
const trail = await s.run(["trail", d.id, "--business", "demo"]);
const raw = s.read("trail", { subject: d.id });
const lines = trail.io.out.text.trimEnd().split("\n");
assert.equal(lines[0], `trail ${d.id}: ${raw.length} row(s)`);
assert.deepEqual(lines.slice(1, 1 + raw.length).map((l) => l.split(" ")[1]), raw.map((r) => `${r.table}#${r.seq}`));
assert.deepEqual(lines.slice(-2), ["task: vikunja:1/7", "follow with: mosaic trail vikunja:1/7"]);
// A decision's trail names its task; it does not pull in the task's rows.
assert.ok(raw.every((r) => r.table !== "task_snapshots"));
});
test("decide refuses without a terminal or --yes, on an unknown option and on a short reference", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor" });
assert.equal((await s.run(["decide", d.id, "yes", "--business", "demo"])).code, 4);
assert.equal((await s.run(["decide", d.id, "maybe", "--yes", "--business", "demo"])).code, 2);
assert.equal((await s.run(["decide", d.id.slice(0, 7), "yes", "--yes", "--business", "demo"])).code, 2);
assert.equal((await s.run(["decide", "ffffffff", "yes", "--yes", "--business", "demo"])).code, 2);
assert.equal(s.read("inbox").length, 1);
assert.ok(s.calls.every((c) => c.verb === "inbox"));
});
test("decide prints a declining choice as declining", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor" });
const { code, io: x } = await s.run(["decide", d.id, "no", "--yes", "--business", "demo"]);
assert.equal(code, 0);
assert.match(x.out.text, /your choice: no \(Decline\); this declines the action/);
});
test("an unknown outcome is reported once and never resent", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor" });
const calls = [];
const transport = () => async (verb) => {
calls.push(verb);
if (verb === "inbox") return s.read("inbox");
const e = new Error("outcome-unknown");
e.code = "outcome-unknown";
throw e;
};
const x = io();
const err = await main(["decide", d.id, "yes", "--yes", "--business", "demo"], x, { system: { dataRoot: s.dataRoot }, transport }).catch((e) => e);
assert.equal(err.exitCode, 1);
assert.match(err.message, new RegExp(`Check mosaic inbox or mosaic trail ${d.id} before trying again`));
assert.deepEqual(calls, ["inbox", "decision.resolve"]);
});
test("a decision closed before the answer arrives exits 2 and points at its trail", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor" });
const transport = () => async (verb) => {
if (verb === "inbox") return s.read("inbox");
const e = new Error("decision-closed");
e.code = "decision-closed";
throw e;
};
const err = await main(["decide", d.id, "yes", "--yes", "--business", "demo"], io(), { system: { dataRoot: s.dataRoot }, transport }).catch((e) => e);
assert.ok(err instanceof CliError);
assert.equal(err.exitCode, 2);
assert.match(err.message, new RegExp(`was closed before your answer arrived; see mosaic trail ${d.id}`));
});
test("a prefix that matches two open decisions exits 2 and resolves neither", async (t) => {
const s = setup(t);
const d = s.raise("git.push.protected", { target: "refactor" });
const twin = { ...structuredClone(s.read("inbox")[0]), id: `${d.id.slice(0, 8)}-ffff-4fff-8fff-ffffffffffff` };
const calls = [];
const transport = () => async (verb) => {
calls.push(verb);
return verb === "inbox" ? [...s.read("inbox"), twin] : null;
};
const err = await main(["decide", d.id.slice(0, 8), "yes", "--yes", "--business", "demo"], io(), { system: { dataRoot: s.dataRoot }, transport }).catch((e) => e);
assert.equal(err.exitCode, 2);
assert.match(err.message, /matches 2 open decisions; use more of the id/);
assert.deepEqual(calls, ["inbox"]);
});
test("without --business a command uses the live host's business, and a stale host.json is not a host", async (t) => {
const s = setup(t);
mkdirSync(hostDir(s.dataRoot), { recursive: true, mode: 0o700 });
const write = (startTime) => writeFileSync(hostFile(s.dataRoot), JSON.stringify({ pid: process.pid, startTime, business: "demo" }), { mode: 0o600 });
write("not-this-process");
assert.equal((await s.run(["inbox"])).code, 4);
assert.equal(s.calls.length, 0);
write(startTimeOf(process.pid));
assert.equal((await s.run(["inbox"])).code, 0);
assert.deepEqual(s.calls.map((c) => c.business), ["demo"]);
});
test("every human command refuses inside an agent run before it touches the bus", async (t) => {
const s = setup(t);
for (const argv of [["inbox"], ["tasks"], ["agents"], ["trail", "vikunja:1/7"], ["decide", "abcdefgh", "yes", "--yes"]]) {
const x = io({ ...io().env, CLAUDECODE: "1" });
const { code } = await s.run([...argv, "--business", "demo"], x);
assert.equal(code, 3, argv.join(" "));
}
assert.equal(s.calls.length, 0);
});
test("usage errors exit 4; no business and no host is a usage error", async (t) => {
const s = setup(t);
for (const argv of [[], ["nope"], ["inbox", "extra", "--business", "demo"], ["inbox", "--bogus"], ["trail", "--business", "demo"], ["decide", "x", "--business", "demo"], ["bus"], ["bus", "stop", "x"], ["inbox"]]) {
assert.equal((await s.run(argv)).code, 4, argv.join(" ") || "(none)");
}
assert.equal(s.calls.length, 0);
});
test("agents and tasks print through the broker", async (t) => {
const s = setup(t);
const agents = await s.run(["agents", "--business", "demo"]);
assert.match(agents.io.out.text, /^coder coder-run pi since /);
const tasks = await s.run(["tasks", "--business", "demo"]);
assert.equal(tasks.io.out.text, "tasks: none\n");
});
test("notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes", (t) => {
const dataRoot = tmp(t);
assert.throws(() => readNotifyConfig(dataRoot, "acme"), (e) => e.exitCode === 3 && /no notifier config/.test(e.message));
const file = notifyConfig(dataRoot, "acme", "sage-seat");
assert.equal(readNotifyConfig(dataRoot, "acme"), "sage-seat");
chmodSync(file, 0o644);
assert.throws(() => readNotifyConfig(dataRoot, "acme"), (e) => e.exitCode === 3 && /mode 0600/.test(e.message));
writeJson(file, { notifyVersion: 1, binding: null, channel: "x" });
assert.throws(() => readNotifyConfig(dataRoot, "acme"), (e) => e.exitCode === 3);
writeJson(file, { notifyVersion: 1, binding: "../escape" });
assert.throws(() => readNotifyConfig(dataRoot, "acme"), (e) => e.exitCode === 3);
writeJson(file, "not json");
assert.throws(() => readNotifyConfig(dataRoot, "acme"), (e) => e.exitCode === 3);
writeJson(file, { notifyVersion: 1, binding: null });
assert.equal(readNotifyConfig(dataRoot, "acme"), null);
assert.equal(readFileSync(join(dataRoot, "notify", "acme", "notify.json"), "utf8").includes("null"), true);
});
+67
View File
@@ -0,0 +1,67 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import { bootConfig, loadSystem } from "../src/config.mjs";
import { writeJson } from "../../business/tests/helpers.mjs";
import { fixture, tmp } from "./helpers.mjs";
test("bootConfig builds the broker's boot message for one business, with no trackers key when no project names one", (t) => {
const f = fixture(tmp(t));
const system = loadSystem({ env: f.env });
const warnings = [];
const boot = bootConfig({ system, businessId: "acme", env: f.env, warn: (w) => warnings.push(w) });
assert.equal(boot.dataRoot, f.dataRoot);
assert.deepEqual(Object.keys(boot.businesses), ["acme"]);
assert.deepEqual(boot.launches, []);
assert.deepEqual(boot.readers, ["acme"]);
assert.equal("trackers" in boot, false);
assert.match(warnings[0], /no project file sets tracker\.project/);
});
test("trackers come from the tracker.* variables of the one project that names a tracker project", (t) => {
const root = tmp(t);
const f = fixture(root);
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
});
test("with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict", (t) => {
const root = tmp(t);
const f = fixture(root, "acme", (doc) => {
doc.projects.web = { root: join(root, "web") };
return doc;
});
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
writeJson(join(root, "web", ".mosaic", "project.json"), { projectVersion: 1, id: "web", vars: {} });
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.deepEqual(boot.trackers, { acme: { baseUrl: "http://127.0.0.1:3456", project: 12, pollSeconds: 60, reconcileMinutes: 60 } });
});
test("two projects that each name a tracker project refuse, since the boot shape holds one", (t) => {
const root = tmp(t);
const f = fixture(root, "acme", (doc) => {
doc.projects.web = { root: join(root, "web") };
return doc;
});
writeJson(join(root, "project", ".mosaic", "project.json"), { projectVersion: 1, id: "stack", vars: { "tracker.project": 12 } });
writeJson(join(root, "web", ".mosaic", "project.json"), { projectVersion: 1, id: "web", vars: { "tracker.project": 13 } });
assert.throws(() => bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env }), (e) => e.exitCode === 3 && /stack, web each set tracker\.project/.test(e.message));
});
test("a business without tracker.baseUrl gets no trackers entry", (t) => {
const f = fixture(tmp(t), "acme", (doc) => {
delete doc.vars["tracker.baseUrl"];
return doc;
});
const warnings = [];
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env, warn: (w) => warnings.push(w) });
assert.equal("trackers" in boot, false);
assert.deepEqual(warnings, []);
});
test("an unknown business and a broken system config refuse with exit 3", (t) => {
const f = fixture(tmp(t));
assert.throws(() => bootConfig({ system: loadSystem({ env: f.env }), businessId: "nope", env: f.env }), (e) => e.exitCode === 3);
assert.throws(() => loadSystem({ env: { ...f.env, MOSAIC_CONFIG: join(f.dataRoot, "missing.json") } }), (e) => e.exitCode === 3);
});
+27
View File
@@ -0,0 +1,27 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { formatAgents, formatInbox, formatTasks, formatTrail } from "../src/format.mjs";
test("empty views say so", () => {
assert.equal(formatInbox([]), "inbox: empty\n");
assert.equal(formatAgents([]), "agents: no role is claimed\n");
assert.equal(formatTasks([]), "tasks: none\n");
});
test("the trail keeps the broker's order and names a decision's task without its rows", () => {
const rows = [
{ at: "2026-10-08T10:00:00Z", table: "decisions", seq: 4, id: "d-1", class: "gated", action: "deploy", route_to: "human", blocking: 1, question: "Ship?", task_ref: "vikunja:1/7" },
{ at: "2026-10-08T09:00:00Z", table: "events", seq: 2, kind: "decision.raised", actor_role: "coder" },
{ at: "2026-10-08T11:00:00Z", table: "decision_events", seq: 1, op: "resolved", choice: "yes", by: "jason", via: "cli" },
];
const lines = formatTrail("d-1", rows).trimEnd().split("\n");
assert.deepEqual(lines.slice(1, 4).map((l) => l.split(" ")[1]), ["decisions#4", "events#2", "decision_events#1"]);
assert.match(lines[1], /gated deploy → human \(blocking\): Ship\?/);
assert.match(lines[3], /resolved choice yes by jason via cli/);
assert.deepEqual(lines.slice(-2), ["task: vikunja:1/7", "follow with: mosaic trail vikunja:1/7"]);
assert.doesNotMatch(formatTrail("vikunja:1/7", rows), /follow with/);
});
test("tasks print the tracker fields the snapshot carries", () => {
assert.equal(formatTasks([{ task_ref: "vikunja:1/7", fields: { title: "Build S4", done: false } }]), "vikunja:1/7 open Build S4\n");
});
+113
View File
@@ -0,0 +1,113 @@
// Shared fixtures. Every test works in its own temporary directory and
// points MOSAIC_CONFIG there; nothing reads the real ~/.config, a real
// token, a real binding, or the real human transport.
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PassThrough } from "node:stream";
import { BusError, Broker } from "../../bus/src/broker.mjs";
import { Store } from "../../bus/src/store.mjs";
import { businessDoc, rolesCopy, systemConfig, writeJson } from "../../business/tests/helpers.mjs";
import { AGENT_MARKERS } from "../src/transport.mjs";
export function tmp(t, prefix = "mosaic-cli-") {
const root = mkdtempSync(join(tmpdir(), prefix));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
// The environment with every agent marker removed: the test runner itself
// may run under an agent, and the commands refuse there.
export function humanEnv(extra = {}) {
const env = { ...process.env, ...extra };
for (const k of AGENT_MARKERS) delete env[k];
delete env.NODE_TEST_CONTEXT;
return env;
}
// A config directory with the system config, the roles and business `id`.
export function fixture(root, id = "acme", edit = (doc) => doc) {
const config = systemConfig(root);
const roles = rolesCopy(root);
const doc = edit(businessDoc(root, id));
writeJson(join(root, "config", "businesses", `${id}.json`), doc);
return { config, roles, dataRoot: join(root, "data"), doc, env: humanEnv({ MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: roles }) };
}
export function notifyConfig(dataRoot, business, binding) {
// The journal sits beside notify.json and refuses a directory looser than 0700.
mkdirSync(join(dataRoot, "notify", business), { recursive: true, mode: 0o700 });
return writeJson(join(dataRoot, "notify", business, "notify.json"), { notifyVersion: 1, binding });
}
// An io for main(): captured stdout and stderr, a stdin that is not a TTY.
export function io(env = humanEnv()) {
const out = { text: "" };
const err = { text: "" };
const stdin = new PassThrough();
stdin.isTTY = false;
return {
env,
stdin,
stdout: { write: (s) => ((out.text += s), true) },
stderr: { write: (s) => ((err.text += s), true) },
out,
err,
};
}
export const OPTIONS = [
{ key: "yes", text: "Allow" },
{ key: "no", text: "Decline" },
];
export const BUSINESSES = {
demo: {
id: "demo",
human: "jason",
arbiters: { technical: "cto", delivery: "pm" },
roles: {
pm: { authority: { withinRole: ["message.send"], crossRole: [] } },
cto: { authority: { withinRole: ["message.send"], crossRole: [] } },
coder: { authority: { withinRole: ["message.send"], crossRole: ["task.scope.change"] } },
},
},
};
// An in-process broker with a claimed coder and the human's capability.
// transport(business) mirrors humanTransport: errors carry the bus code.
export function broker(t) {
const root = tmp(t, "mosaic-cli-bus-");
const store = new Store(root);
t.after(() => store.close());
const b = new Broker({ store, businesses: BUSINESSES });
const coder = b.bindLaunch({ business: "demo", role: "coder", run: "coder-run", harness: "pi", address: "coder-run" });
b.request(coder, { verb: "role.claim" });
const human = b.bindHuman({ business: "demo", human: "jason", via: "cli", outsideAgent: true });
const reader = b.bindReader({ business: "demo" });
const calls = [];
const transport = (business) => async (verb, args = {}) => {
calls.push({ business, verb, args });
try {
return structuredClone(b.request(human, { verb, args }));
} catch (e) {
if (!(e instanceof BusError)) throw e;
const error = new Error(e.code);
error.code = e.code;
throw error;
}
};
const raise = (action, extra = {}) =>
b.request(coder, { verb: "decision.raise", args: { action, question: "Push the release?", options: OPTIONS, recommendation: "no", blocking: false, ...extra } });
return { b, store, coder, human, reader, transport, calls, raise, read: (verb, args = {}) => structuredClone(b.request(reader, { verb, args })) };
}
// A script standing in for packages/bus/src/human-cli.mjs: it reads one
// request on stdin and answers from the table in its first argument.
export function fakeCli(root, body) {
const file = join(root, "fake-human-cli.mjs");
writeFileSync(file, body);
chmodSync(file, 0o600);
return file;
}
+222
View File
@@ -0,0 +1,222 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { createServer } from "node:http";
import { once } from "node:events";
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "../../bus/src/client.mjs";
import { bootConfig, loadSystem, REPO } from "../src/config.mjs";
import { hostDir, hostFile, hostStatus, startHost, startTimeOf, stopHost, watchChildren } from "../src/host.mjs";
import { journalPath } from "../src/notifier.mjs";
import { IDS, makeDeployment } from "../../discord/tests/helpers.mjs";
import { fixture, notifyConfig, OPTIONS, tmp } from "./helpers.mjs";
const CLI = fileURLToPath(new URL("../src/cli.mjs", import.meta.url));
const CHANNEL = "100000000000000900";
const TOKEN = "MTAw.abcdefghijklmnopqrstuvwxyz0123456789";
// A fake Discord REST on 127.0.0.1: opens one DM channel, accepts messages.
async function fakeDiscord(t) {
const requests = [];
let n = 0;
const server = createServer((req, res) => {
let body = "";
req.on("data", (b) => (body += b));
req.on("end", () => {
requests.push({ method: req.method, url: req.url, body: body ? JSON.parse(body) : null, authorized: req.headers.authorization === `Bot ${TOKEN}` });
res.setHeader("content-type", "application/json");
if (req.url === "/users/@me/channels") return res.end(JSON.stringify({ id: CHANNEL, type: 1 }));
if (req.url === `/channels/${CHANNEL}/messages`) return res.end(JSON.stringify({ id: `30000000000000${String(++n).padStart(4, "0")}` }));
res.statusCode = 404;
res.end("{}");
});
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
t.after(() => server.close());
return { base: `http://127.0.0.1:${server.address().port}`, requests, dms: () => requests.filter((r) => r.url.endsWith("/messages") && r.body.nonce.startsWith("dm")) };
}
async function until(fn, ms = 8000) {
const end = Date.now() + ms;
while (Date.now() < end) {
if (fn()) return;
await new Promise((r) => setTimeout(r, 50));
}
throw new Error("timed out waiting");
}
const procText = (pid, what) => {
try {
return readFileSync(`/proc/${pid}/${what}`, "utf8");
} catch {
return "";
}
};
test("the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once", async (t) => {
const root = tmp(t);
const f = fixture(root);
makeDeployment(root, { dmRecipient: IDS.owner });
const discord = await fakeDiscord(t);
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
assert.equal("trackers" in boot, false);
const logs = [];
const host = await startHost({ boot, business: "acme", notifier: { binding: "test-seat", base: discord.base, pollMs: 100 }, log: (l) => logs.push(l) });
t.after(() => host.close(0));
const state = JSON.parse(readFileSync(hostFile(f.dataRoot), "utf8"));
assert.equal(statSync(hostFile(f.dataRoot)).mode & 0o777, 0o600);
assert.deepEqual(Object.keys(state).sort(), ["business", "hostVersion", "notifier", "pid", "startTime", "startedAt"]);
assert.equal(hostStatus(f.dataRoot).host.live, true);
const launch = await host.bindLaunch({ business: "acme", role: "coder", run: "coder-run", harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) });
assert.equal(launch.run, "coder-run");
const coder = new Client({ path: host.path, cap: launch.cap });
await coder.call("role.claim");
const d = await coder.call("decision.raise", { action: "git.push.protected", target: "refactor", question: "Push?", options: OPTIONS, recommendation: "no", blocking: true, task_ref: "vikunja:1/7" });
await until(() => discord.dms().length === 1);
await new Promise((r) => setTimeout(r, 500));
assert.equal(discord.dms().length, 1, "five more polls send nothing new");
assert.ok(discord.requests.every((r) => r.authorized));
assert.match(discord.dms()[0].body.content, new RegExp(`mosaic decide ${d.id.slice(0, 8)}`));
// No capability in a child's argv or environment, or in the state file.
for (const pid of Object.values(host.pids)) {
assert.ok(!procText(pid, "cmdline").includes(launch.cap));
assert.ok(!procText(pid, "environ").includes(launch.cap));
}
assert.ok(!readFileSync(hostFile(f.dataRoot), "utf8").includes(launch.cap));
assert.equal(await host.close(0), 0);
const journal = readFileSync(journalPath(f.dataRoot, "acme"), "utf8");
for (const id of [IDS.owner, CHANNEL, TOKEN]) assert.ok(!journal.includes(id));
assert.equal(journal.trim().split("\n").filter((l) => JSON.parse(l).kind === "dm").length, 1);
assert.equal(existsSync(hostFile(f.dataRoot)), false);
assert.equal(existsSync(join(f.dataRoot, "bus", "writer.lock")), false);
});
test("a notifier that dies takes the host down with exit 1, so the unit restarts the pair", async (t) => {
const root = tmp(t);
const f = fixture(root);
makeDeployment(root, { dmRecipient: IDS.owner });
const discord = await fakeDiscord(t);
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
const logs = [];
const host = await startHost({ boot, business: "acme", notifier: { binding: "test-seat", base: discord.base, pollMs: 100 }, log: (l) => logs.push(l) });
process.kill(host.pids.notifier, "SIGKILL");
assert.equal(await host.done, 1);
assert.match(logs.join("\n"), /notifier exited \(SIGKILL\); stopping the host/);
assert.equal(existsSync(join(f.dataRoot, "bus", "writer.lock")), false);
assert.equal(existsSync(hostFile(f.dataRoot)), false);
});
test("a notifier that refuses stops the broker and the host refuses with exit 3", async (t) => {
const root = tmp(t);
const f = fixture(root);
makeDeployment(root);
const boot = bootConfig({ system: loadSystem({ env: f.env }), businessId: "acme", env: f.env });
const started = startHost({ boot, business: "acme", notifier: { binding: "test-seat" }, log: () => {} });
// If the refusal regresses, the host starts; close it so the file still ends.
t.after(async () => (await started.catch(() => null))?.close(0));
await assert.rejects(started, (e) => e.exitCode === 3 && /no dmRecipient/.test(e.message));
assert.equal(existsSync(join(f.dataRoot, "bus", "writer.lock")), false);
assert.equal(existsSync(hostFile(f.dataRoot)), false);
});
test("watchChildren reports a child that died before it was called, and one that dies later", async (t) => {
const early = spawn(process.execPath, ["-e", "process.exit(7)"], { stdio: "ignore" });
await once(early, "exit");
const late = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" });
t.after(() => late.kill("SIGKILL"));
await once(late, "spawn");
const deaths = [];
watchChildren({ broker: early, notifier: late, none: null }, (...d) => deaths.push(d));
assert.deepEqual(deaths, [["broker", 7, null]], "the exit before the watch is not lost");
late.kill("SIGTERM");
await once(late, "exit");
assert.deepEqual(deaths, [["broker", 7, null], ["notifier", null, "SIGTERM"]]);
});
test("bus stop refuses to signal a live pid that is not a bus host", async (t) => {
const dataRoot = tmp(t);
const child = spawn(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], { stdio: "ignore" });
t.after(() => child.kill("SIGKILL"));
await once(child, "spawn");
mkdirSync(hostDir(dataRoot), { recursive: true, mode: 0o700 });
writeFileSync(hostFile(dataRoot), JSON.stringify({ pid: child.pid, startTime: startTimeOf(child.pid), business: "acme" }), { mode: 0o600 });
await assert.rejects(stopHost(dataRoot, { timeoutMs: 1000 }), (e) => e.exitCode === 3 && /is not a bus host; refusing to signal it/.test(e.message));
await new Promise((r) => setTimeout(r, 200));
assert.equal(child.exitCode, null);
assert.equal(child.signalCode, null, "the child was not signalled");
});
test("bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock", (t) => {
const f = fixture(tmp(t));
notifyConfig(f.dataRoot, "acme", null);
mkdirSync(join(f.dataRoot, "bus"), { recursive: true, mode: 0o700 });
writeFileSync(join(f.dataRoot, "bus", "writer.lock"), JSON.stringify({ pid: 999999999, at: "2026-10-08T00:00:00Z" }), { mode: 0o600 });
const r = spawnSync(process.execPath, [CLI, "bus", "start", "acme"], { env: f.env, encoding: "utf8", timeout: 40000 });
assert.equal(r.status, 3, r.stderr);
assert.match(r.stderr, /broker refused to start: startup-refused/);
const s = spawnSync(process.execPath, [CLI, "bus", "status"], { env: f.env, encoding: "utf8" });
assert.equal(s.status, 0, s.stderr);
assert.match(s.stdout, /host: none/);
assert.match(s.stdout, /writer\.lock: pid 999999999 \(not running/);
});
test("bus start refuses with exit 3 without a notifier config", (t) => {
const f = fixture(tmp(t));
const r = spawnSync(process.execPath, [CLI, "bus", "start", "acme"], { env: f.env, encoding: "utf8", timeout: 40000 });
assert.equal(r.status, 3);
assert.match(r.stderr, /no notifier config/);
assert.equal(existsSync(join(f.dataRoot, "bus", "writer.lock")), false);
});
test("bus start runs until bus stop; status reports it while it runs", async (t) => {
const f = fixture(tmp(t));
notifyConfig(f.dataRoot, "acme", null);
const child = spawn(process.execPath, [CLI, "bus", "start", "acme"], { env: f.env, stdio: ["ignore", "pipe", "pipe"] });
t.after(() => child.exitCode === null && child.kill("SIGKILL"));
let out = "";
child.stdout.on("data", (b) => (out += b));
child.stderr.on("data", (b) => (out += b));
await until(() => /bus host up: business acme/.test(out), 30000);
const status = spawnSync(process.execPath, [CLI, "bus", "status", "--json"], { env: f.env, encoding: "utf8" });
const s = JSON.parse(status.stdout);
assert.equal(s.host.live, true);
assert.equal(s.host.pid, child.pid);
assert.equal(s.host.notifier, null);
assert.equal(s.socket, true);
assert.equal(s.writerLock.live, true);
const exit = once(child, "exit");
const stop = spawnSync(process.execPath, [CLI, "bus", "stop"], { env: f.env, encoding: "utf8", timeout: 70000 });
assert.equal(stop.status, 0, stop.stderr);
assert.match(stop.stdout, new RegExp(`stopped bus host for acme \\(pid ${child.pid}\\)`));
assert.equal((await exit)[0], 0, out);
assert.equal(existsSync(join(f.dataRoot, "bus", "writer.lock")), false);
const again = spawnSync(process.execPath, [CLI, "bus", "stop"], { env: f.env, encoding: "utf8" });
assert.match(again.stdout, /no host runs/);
});
test("bus-service.sh renders the unit and installs it into a given directory", (t) => {
const dir = tmp(t);
const script = join(REPO, "scripts", "bus-service.sh");
const render = spawnSync(script, ["render"], { encoding: "utf8" });
assert.equal(render.status, 0, render.stderr);
assert.match(render.stdout, new RegExp(`ExecStart=${REPO.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/scripts/mosaic bus start %i`));
assert.match(render.stdout, /RestartPreventExitStatus=2 3 4/);
assert.doesNotMatch(render.stdout, /@REPO@|@PATH@/);
// A user unit cannot order on a system target (Darkwing F3).
assert.doesNotMatch(render.stdout, /network-online/);
const first = spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8" });
assert.equal(first.status, 0, first.stderr);
assert.match(first.stdout, /written: /);
assert.equal(readFileSync(join(dir, "[email protected]"), "utf8"), render.stdout);
assert.match(spawnSync(script, ["install", "--dir", dir, "--no-reload"], { encoding: "utf8" }).stdout, /unchanged: /);
assert.match(spawnSync(script, ["uninstall", "--dir", dir, "--no-reload"], { encoding: "utf8" }).stdout, /removed: /);
assert.equal(spawnSync(script, ["bogus"], { encoding: "utf8" }).status, 4);
});
+244
View File
@@ -0,0 +1,244 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createNotifier, digestContent, digestNonce, dmNonce, journalPath, openJournal, runLoop, zoned } from "../src/notifier.mjs";
import { RestOutcome } from "../../discord/src/rest.mjs";
import { broker, tmp } from "./helpers.mjs";
// Discord-side fake: records sends, answers from a script (default: ok).
function fakeDirect(script = []) {
const sends = [];
let n = 0;
return {
sends,
async send(m) {
sends.push(m);
const next = script.shift() ?? "ok";
if (next === "ok") return { messageId: `30000000000000${String(++n).padStart(4, "0")}` };
throw new RestOutcome(next, `dm: ${next}`, { status: next === "refused" ? 403 : null });
},
};
}
// 2026-10-08 is CDT (UTC-5): 13:00Z is 08:00 Chicago.
const at = (iso) => {
const clock = { t: new Date(iso) };
return { clock, now: () => clock.t, advance: (ms) => (clock.t = new Date(clock.t.getTime() + ms)) };
};
function setup(t, iso, script) {
const bus = broker(t);
const dataRoot = tmp(t);
const direct = fakeDirect(script);
const time = at(iso);
const logs = [];
const make = () => createNotifier({ business: "demo", dataRoot, inbox: async () => bus.read("inbox"), direct, now: time.now, log: (l) => logs.push(l) });
return { ...bus, dataRoot, direct, time, logs, make, notifier: make(), journal: () => readFileSync(journalPath(dataRoot, "demo"), "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)) };
}
test("zoned uses the IANA zone across DST", () => {
assert.deepEqual(zoned(new Date("2026-10-08T13:00:00Z")), { day: "2026-10-08", hour: 8 });
assert.deepEqual(zoned(new Date("2026-12-08T13:00:00Z")), { day: "2026-12-08", hour: 7 });
assert.deepEqual(zoned(new Date("2026-10-09T04:59:00Z")), { day: "2026-10-08", hour: 23 });
assert.throws(() => zoned(new Date(), "Not/AZone"), RangeError);
});
test("each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not", async (t) => {
const s = setup(t, "2026-10-08T12:00:00Z");
const blocking = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
s.raise("deploy", { target: "staging" });
assert.deepEqual(await s.notifier.tick(), { dms: 1, digest: false, failed: 0 });
assert.deepEqual(await s.notifier.tick(), { dms: 0, digest: false, failed: 0 });
assert.deepEqual(await s.make().tick(), { dms: 0, digest: false, failed: 0 });
assert.equal(s.direct.sends.length, 1);
const [dm] = s.direct.sends;
assert.equal(dm.nonce, dmNonce(blocking.id));
assert.ok(dm.nonce.length <= 25);
assert.match(dm.content, /a blocking decision needs you/);
assert.match(dm.content, /choosing "yes" authorizes it/);
assert.match(dm.content, /task vikunja:1\/7/);
assert.match(dm.content, new RegExp(`mosaic decide ${blocking.id.slice(0, 8)} <option>`));
const [rec] = s.journal();
assert.deepEqual(Object.keys(rec).sort(), ["at", "decision", "kind", "messageId", "outcome"]);
assert.equal(rec.outcome, "confirmed");
assert.equal(rec.decision, blocking.id);
assert.equal(statSync(journalPath(s.dataRoot, "demo")).mode & 0o777, 0o600);
});
test("two blocking decisions get two DMs with different nonces", async (t) => {
const s = setup(t, "2026-10-08T12:00:00Z");
const a = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
const b = s.raise("git.push.protected", { target: "main", blocking: true, task_ref: "vikunja:1/8" });
assert.equal((await s.notifier.tick()).dms, 2);
const nonces = s.direct.sends.map((m) => m.nonce);
assert.deepEqual(nonces, [dmNonce(a.id), dmNonce(b.id)]);
assert.notEqual(nonces[0], nonces[1]);
});
test("the digest nonce differs per business and per day and fits Discord's 25 characters", () => {
const n = digestNonce("demo", "2026-10-08");
assert.ok(n.startsWith("dg") && n.length <= 25);
assert.notEqual(n, digestNonce("acme", "2026-10-08"));
assert.notEqual(n, digestNonce("demo", "2026-10-09"));
});
test("a failed DM is journaled, backs off, and is retried until it lands", async (t) => {
const s = setup(t, "2026-10-08T12:00:00Z", ["unknown", "refused"]);
s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
assert.equal((await s.notifier.tick()).failed, 1);
s.time.advance(10_000);
assert.equal((await s.notifier.tick()).failed, 0, "inside the first 30 s backoff");
s.time.advance(25_000);
assert.equal((await s.notifier.tick()).failed, 1, "second attempt refused");
s.time.advance(45_000);
assert.equal((await s.notifier.tick()).dms, 0, "inside the 60 s backoff");
s.time.advance(20_000);
assert.equal((await s.notifier.tick()).dms, 1);
assert.deepEqual(s.journal().map((r) => r.outcome), ["unknown", "refused", "confirmed"]);
assert.equal(s.journal()[1].status, 403);
assert.equal(new Set(s.direct.sends.map((m) => m.nonce)).size, 1, "every retry reuses the nonce");
assert.ok(s.logs.some((l) => /retry in 30 s/.test(l)));
});
test("the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd", async (t) => {
const s = setup(t, "2026-10-08T12:59:00Z");
const d = s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
s.raise("deploy", { target: "staging" });
assert.equal((await s.notifier.tick()).digest, false, "07:59 is before the digest");
s.time.advance(60_000);
assert.equal((await s.notifier.tick()).digest, true);
assert.equal((await s.notifier.tick()).digest, false, "one a day");
const digest = s.direct.sends.at(-1);
assert.equal(digest.nonce, digestNonce("demo", "2026-10-08"));
assert.match(digest.content, /^Mosaic digest \(demo, 2026-10-08\): 2 open decision\(s\)\./);
assert.match(digest.content, new RegExp(`\\[blocking, DM sent\\] ${d.id.slice(0, 8)} git\\.push\\.protected`));
assert.match(digest.content, /- [0-9a-f]{8} deploy: /);
assert.match(digest.content, /Run mosaic inbox for the full list\.$/);
assert.deepEqual(s.journal().at(-1), { ...s.journal().at(-1), kind: "digest", decision: null, day: "2026-10-08", outcome: "confirmed" });
s.time.advance(24 * 3600_000);
assert.equal((await s.notifier.tick()).digest, true, "the next day has its own");
});
test("a late start with no digest for the day sends one at once; an empty inbox gets one line", async (t) => {
const s = setup(t, "2026-10-08T21:30:00Z");
assert.deepEqual(await s.notifier.tick(), { dms: 0, digest: true, failed: 0 });
assert.equal(s.direct.sends[0].content, "Mosaic digest (demo, 2026-10-08): your inbox is empty.");
assert.equal((await s.make().tick()).digest, false, "a restart reads the day from the journal");
});
test("an inbox read failure is logged and the next poll retries", async (t) => {
const dataRoot = tmp(t);
let fail = true;
const n = createNotifier({ business: "demo", dataRoot, inbox: async () => { if (fail) { const e = new Error("x"); e.code = "outcome-unknown"; throw e; } return []; }, direct: fakeDirect(), now: () => new Date("2026-10-08T12:00:00Z"), log: () => {} });
assert.equal((await n.tick()).inboxError, true);
fail = false;
assert.equal((await n.tick()).inboxError, undefined);
});
test("no Discord id reaches the journal or the log", async (t) => {
const s = setup(t, "2026-10-08T13:00:00Z", ["refused"]);
s.raise("git.push.protected", { target: "refactor", blocking: true, task_ref: "vikunja:1/7" });
await s.notifier.tick();
const text = readFileSync(journalPath(s.dataRoot, "demo"), "utf8") + s.logs.join("\n");
assert.doesNotMatch(text, /channel|recipient|user/i);
for (const r of s.journal()) assert.ok(r.messageId === null || /^[0-9]+$/.test(r.messageId));
});
const tornFiles = (file) => readdirSync(dirname(file)).filter((n) => /^torn-\d{8}T\d{9}Z(-\d+)?\.bin$/.test(n)).sort();
const FRAGMENT = '{"at":"x","kind":"dm","dec';
test("the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly", (t) => {
const file = journalPath(tmp(t), "demo");
openJournal(file).append({ at: "x", kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
const good = readFileSync(file);
appendFileSync(file, FRAGMENT);
const logs = [];
const j = openJournal(file, { log: (l) => logs.push(l), now: () => new Date("2026-10-08T23:52:12.345Z") });
assert.deepEqual([...j.sent], ["a"]);
assert.deepEqual(tornFiles(file), ["torn-20261008T235212345Z.bin"]);
const copy = join(dirname(file), "torn-20261008T235212345Z.bin");
assert.equal(readFileSync(copy, "utf8"), FRAGMENT);
assert.equal(statSync(copy).mode & 0o777, 0o600);
assert.deepEqual(readFileSync(file), good, "truncated to its last newline");
assert.equal(logs.length, 2);
assert.match(logs[0], /copied a torn final line \(26 bytes\) to torn-20261008T235212345Z\.bin/);
assert.match(logs[1], /truncated .* to its last newline/);
j.append({ at: "y", kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
const again = [];
assert.deepEqual([...openJournal(file, { log: (l) => again.push(l) }).sent], ["a", "b"]);
assert.deepEqual(again, [], "nothing torn the second time");
});
test("the journal: a crash between the copy and the truncate leaves a tail the next open repairs", (t) => {
const file = journalPath(tmp(t), "demo");
openJournal(file).append({ at: "x", kind: "dm", decision: "a", outcome: "confirmed", messageId: "1" });
appendFileSync(file, FRAGMENT);
const now = () => new Date("2026-10-08T23:52:12.345Z");
// The log after step 1 throws: the process dies before step 2.
assert.throws(() => openJournal(file, { now, log: () => { throw new Error("crash"); } }), /crash/);
assert.ok(readFileSync(file, "utf8").endsWith(FRAGMENT), "still torn");
assert.deepEqual(tornFiles(file), ["torn-20261008T235212345Z.bin"]);
const j = openJournal(file, { now });
assert.deepEqual(tornFiles(file), ["torn-20261008T235212345Z-1.bin", "torn-20261008T235212345Z.bin"], "a second copy, the first kept");
for (const n of tornFiles(file)) assert.equal(readFileSync(join(dirname(file), n), "utf8"), FRAGMENT);
j.append({ at: "y", kind: "dm", decision: "b", outcome: "confirmed", messageId: "2" });
assert.deepEqual([...openJournal(file).sent], ["a", "b"]);
});
test("the journal: a whole file that is one torn line truncates to empty", (t) => {
const file = journalPath(tmp(t), "demo");
openJournal(file);
writeFileSync(file, FRAGMENT);
assert.equal(openJournal(file).sent.size, 0);
assert.equal(readFileSync(file, "utf8"), "");
assert.equal(tornFiles(file).length, 1);
});
test("the journal: a malformed complete line refuses and leaves the file and any torn tail alone", (t) => {
const file = journalPath(tmp(t), "demo");
openJournal(file);
writeFileSync(file, `garbage\n${FRAGMENT}`);
assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && /line 1 is malformed/.test(e.message));
assert.equal(readFileSync(file, "utf8"), `garbage\n${FRAGMENT}`);
assert.equal(tornFiles(file).length, 0);
});
test("the journal: a loose file mode, a loose directory or a symlinked journal refuses", (t) => {
const root = tmp(t);
const file = journalPath(root, "demo");
openJournal(file);
chmodSync(file, 0o644);
assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && /mode 0600/.test(e.message));
chmodSync(file, 0o600);
chmodSync(dirname(file), 0o755);
assert.throws(() => openJournal(file), (e) => e.exitCode === 3 && /directory must be mode 0700/.test(e.message));
chmodSync(dirname(file), 0o700);
const other = join(root, "elsewhere.jsonl");
writeFileSync(other, "", { mode: 0o600 });
const linked = journalPath(root, "linked");
mkdirSync(dirname(linked), { mode: 0o700 });
symlinkSync(other, linked);
assert.throws(() => openJournal(linked), (e) => e.exitCode === 3 && /must not be a symlink/.test(e.message));
});
test("digest content stays within Discord's 2000 characters", () => {
const inbox = Array.from({ length: 60 }, (_, i) => ({ id: `${String(i).padStart(8, "0")}-x`, action: "deploy", question: "q".repeat(300), blocking: i % 2 === 0 }));
const text = digestContent("demo", "2026-10-08", inbox, () => true);
assert.ok(text.length <= 2000, String(text.length));
assert.match(text, /… and \d+ more\.\nRun mosaic inbox for the full list\.$/);
});
test("runLoop never overlaps ticks and stops after the one in flight", async () => {
let active = 0;
let max = 0;
let count = 0;
const loop = runLoop({ tick: async () => { active++; max = Math.max(max, active); count++; await new Promise((r) => setTimeout(r, 15)); active--; } }, { pollMs: 1 });
await new Promise((r) => setTimeout(r, 80));
await loop.stop();
const after = count;
await new Promise((r) => setTimeout(r, 30));
assert.equal(max, 1);
assert.ok(after >= 2);
assert.equal(count, after);
});
+50
View File
@@ -0,0 +1,50 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { busExit, humanTransport, refuseInsideAgent } from "../src/transport.mjs";
import { fakeCli, humanEnv, tmp } from "./helpers.mjs";
// The fake records argv and stdin, then answers per verb. The real
// human-cli.mjs is never run here: its proof needs a human shell.
const BODY = `
import { readFileSync, writeFileSync } from "node:fs";
const input = readFileSync(0, "utf8");
writeFileSync(process.env.FAKE_LOG, JSON.stringify({ argv: process.argv.slice(2), input }));
const { verb } = JSON.parse(input);
if (verb === "inbox") process.stdout.write(JSON.stringify([{ id: "d1" }]) + "\\n");
else if (verb === "garbage") process.stdout.write("not json");
else if (verb === "hang") setTimeout(() => {}, 60000);
else { process.stderr.write("some noise\\n" + verb + "\\n"); process.exit(2); }
`;
function setup(t) {
const root = tmp(t);
const log = join(root, "log.json");
const cli = fakeCli(root, BODY);
const call = humanTransport({ socket: "/run/fake.sock", business: "acme", env: humanEnv({ FAKE_LOG: log }), cli, timeoutMs: 2000 });
return { call, seen: () => JSON.parse(readFileSync(log, "utf8")) };
}
test("the transport writes {business, verb, args} to the child and reads its JSON", async (t) => {
const s = setup(t);
assert.deepEqual(await s.call("inbox"), [{ id: "d1" }]);
assert.deepEqual(s.seen().argv, ["/run/fake.sock"]);
assert.deepEqual(JSON.parse(s.seen().input), { business: "acme", verb: "inbox", args: {} });
});
test("a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems", async (t) => {
const s = setup(t);
for (const [code, exit] of [["human-required", 3], ["read-only", 3], ["decision-closed", 2], ["outcome-unknown", 1]]) {
await assert.rejects(s.call(code), (e) => e.code === code && e.exitCode === exit, code);
}
await assert.rejects(s.call("garbage"), (e) => e.code === "invalid-response" && e.exitCode === 1);
await assert.rejects(s.call("hang"), (e) => e.exitCode === 1 && /outcome-unknown/.test(e.message));
});
test("busExit and refuseInsideAgent", () => {
assert.equal(busExit("unauthenticated"), 3);
assert.equal(busExit("invalid-request"), 2);
assert.doesNotThrow(() => refuseInsideAgent(humanEnv()));
assert.throws(() => refuseInsideAgent({ PI_AGENT_DIR: "/x" }), (e) => e.exitCode === 3 && /PI_AGENT_DIR/.test(e.message));
});