1. Stale bus host lock refusals from bus status, start, stop and restart name mosaic bus recover ORG. 2. org definitions: unknown org, unknown or malformed name and bad id give one refusal or usage line, no stack trace, no host path. 3. Pack, board and workflow refusals print their hint as template save does; tested with the real CliError transport shape. 4. console start and stop clear a console/start.lock whose named supervisor (pid and start time) is gone; a live or unnamed holder refuses. 5. Template save keeps free text verbatim and writes no placeholders; the preview lists each field that mentions an org-specific name. 6. A request the shim never sent (no socket, connect refused) exits 5 and the CLI reports bus-not-running with the start and recover line. 7. autonomy-precondition carries the check that failed, including on the hire paths. 8. Saving under an existing template name refuses template-exists with a next step; refusal table entry added, channel-proof pins moved.
80 lines
4.5 KiB
JavaScript
80 lines
4.5 KiB
JavaScript
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);
|
|
}
|
|
});
|