import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { busExit, humanTransport, refuseInsideAgent, HUMAN_CLI } 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 if (verb === "not-sent") { process.stderr.write("outcome-unknown\\n"); process.exit(5); } else if (verb === "not-sent-other") { process.stderr.write("human-required\\n"); process.exit(5); } 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); // Row 239 (#1745): a write whose outcome is unknown after its revoke exits as outcome-unknown does. assert.equal(busExit("outcome-unknown-revoked"), 1); assert.doesNotThrow(() => refuseInsideAgent(humanEnv())); assert.throws(() => refuseInsideAgent({ PI_AGENT_DIR: "/x" }), (e) => e.exitCode === 3 && /PI_AGENT_DIR/.test(e.message)); }); // E2E6 fix 6: the shim exits 5 when its connect failed before the request was written. That is a stopped // bus: mosaic says so with the start and recover line. outcome-unknown stays for a request that left. test("E2E6 exit 5 with outcome-unknown is bus-not-running; exit 2 with outcome-unknown is unchanged", async (t) => { const s = setup(t); await assert.rejects(s.call("not-sent"), (e) => e.code === "bus-not-running" && e.exitCode === 3 && e.message === "bus-not-running: the bus for acme is not running, so the request was not sent" && e.next === "start it with mosaic bus start acme, or clear a host that died with mosaic bus recover acme"); await assert.rejects(s.call("outcome-unknown"), (e) => e.code === "outcome-unknown" && e.exitCode === 1 && e.message === "outcome-unknown" && e.next === undefined); // Exit 5 says only that nothing was sent; any other code it carries keeps its own meaning. await assert.rejects(s.call("not-sent-other"), (e) => e.code === "human-required" && e.exitCode === 3); assert.equal(busExit("bus-not-running"), 3); }); // The real shim, with no bus behind the path: nothing reaches a broker, so no proof runs. A missing socket // and a path that is not a listening socket are both a bus that is not running. test("E2E6 the real human shim against a missing socket or a plain file says the bus is not running", async (t) => { const root = tmp(t); const plain = join(root, "plain.sock"); writeFileSync(plain, ""); for (const socket of [join(root, "missing.sock"), plain]) { const call = humanTransport({ socket, business: "acme", env: humanEnv(), cli: HUMAN_CLI, timeoutMs: 20000 }); await assert.rejects(call("inbox"), (e) => e.code === "bus-not-running" && e.exitCode === 3, socket); } });