221 lines
12 KiB
JavaScript
221 lines
12 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { main } from "../src/cli.mjs";
|
|
import { HUMAN_CLI, TESTER_CLI, busExit, testerTransport } from "../src/transport.mjs";
|
|
import { BusError, Broker } from "../../bus/src/broker.mjs";
|
|
import { Store } from "../../bus/src/store.mjs";
|
|
import { BUSINESSES, humanEnv, io, tmp, orgFixture } from "./helpers.mjs";
|
|
|
|
// Row 110 (#1604): --tester binds as the business's tester instead of as the
|
|
// human. The in-process broker here has bus.tester on; the real tester-cli.mjs
|
|
// runs in packages/bus/tests/tester.test.mjs.
|
|
function setup(t, extra = {}) {
|
|
const store = new Store(tmp(t, "mosaic-cli-tester-"));
|
|
t.after(() => store.close());
|
|
const businesses = { demo: { ...BUSINESSES.demo, vars: { "bus.tester": true }, ...extra } };
|
|
const b = new Broker({ store, 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 tester = b.bindTester({ business: "demo" });
|
|
const calls = { human: [], tester: [] };
|
|
const via = (cap, list) => (business) => async (verb, args = {}) => {
|
|
list.push({ business, verb, args });
|
|
try {
|
|
return structuredClone(b.request(cap, { verb, args }));
|
|
} catch (e) {
|
|
if (!(e instanceof BusError)) throw e;
|
|
const error = new Error(e.code);
|
|
error.code = e.code;
|
|
throw error;
|
|
}
|
|
};
|
|
const deps = { system: { dataRoot: tmp(t) }, transport: via(human, calls.human), testerTransport: via(tester, calls.tester), talkPollMs: 5 };
|
|
return { b, store, coder, human, calls, deps };
|
|
}
|
|
// The tester runs inside an agent: a T3 seat or Mos.
|
|
const inAgent = () => io({ ...humanEnv(), CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "sdk-ts" });
|
|
|
|
test("talk --tester sends a message as tester from inside an agent and prints the role's reply", async (t) => {
|
|
const s = setup(t);
|
|
const x = inAgent();
|
|
const deps = {
|
|
...s.deps,
|
|
testerTransport: (business) => {
|
|
const inner = s.deps.testerTransport(business);
|
|
return async (verb, args) => {
|
|
if (verb === "message.receive") {
|
|
const ask = s.store.get("SELECT id FROM messages WHERE from_role='tester'");
|
|
if (ask && !s.store.get("SELECT 1 FROM messages WHERE to_role='tester'")) {
|
|
s.b.request(s.coder, { verb: "message.receive", args: {} });
|
|
s.b.request(s.coder, { verb: "message.send", args: { to: "tester", body: "the plan is current", in_reply_to: ask.id } });
|
|
}
|
|
}
|
|
return inner(verb, args);
|
|
};
|
|
},
|
|
};
|
|
await main(["talk", "coder", "is the plan current?", "--tester", "--business", "demo"], x, deps);
|
|
assert.match(x.out.text, /^sent [0-9a-f-]{8} to coder\n/);
|
|
assert.match(x.out.text, /coder \(INFO, re [0-9a-f-]{8}\) [0-9a-f-]{8}:\nthe plan is current\n/);
|
|
assert.deepEqual(s.calls.human, []);
|
|
assert.deepEqual(s.calls.tester.map((c) => c.verb), ["message.send", "message.receive", "message.read"]);
|
|
const row = s.store.get("SELECT * FROM messages WHERE from_role='tester'");
|
|
assert.equal(row.from_run, "tester-cli");
|
|
assert.equal(row.class, "REQUEST");
|
|
assert.deepEqual(s.b.request(s.human, { verb: "message.receive", args: {} }), []);
|
|
assert.equal(s.store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n, 0);
|
|
});
|
|
|
|
// Row 175 (#1673, mos-0872 Q1): the test system needs no "[mos test]" prefix; the body goes as typed.
|
|
test("talk --tester sends text with no prefix, and follow --tester reads", async (t) => {
|
|
const s = setup(t);
|
|
await main(["talk", "coder", "is the plan current?", "--tester", "--wait", "0", "--business", "demo"], inAgent(), s.deps);
|
|
assert.deepEqual(s.calls.tester.map((c) => c.verb), ["message.send"]);
|
|
assert.equal(s.store.get("SELECT body FROM messages WHERE from_role='tester'").body, "is the plan current?");
|
|
s.calls.tester.length = 0;
|
|
const controller = new AbortController();
|
|
const deps = { ...s.deps, signal: controller.signal, testerTransport: (b) => async (verb, args) => (controller.abort(), s.deps.testerTransport(b)(verb, args)) };
|
|
await main(["follow", "--tester", "--business", "demo"], inAgent(), deps);
|
|
assert.deepEqual(s.calls.tester.map((c) => c.verb), ["message.receive"]);
|
|
});
|
|
|
|
test("the read verbs take --tester inside an agent, and the trail shows the tester as the sender", async (t) => {
|
|
const s = setup(t);
|
|
await main(["talk", "coder", "[mos test] hello", "--tester", "--wait", "0", "--business", "demo"], inAgent(), s.deps);
|
|
const id = s.store.get("SELECT id FROM messages WHERE from_role='tester'").id;
|
|
for (const verb of ["inbox", "tasks", "agents"]) {
|
|
const x = inAgent();
|
|
await main([verb, "--tester", "--business", "demo"], x, s.deps);
|
|
assert.ok(x.out.text.length > 0, verb);
|
|
}
|
|
const x = inAgent();
|
|
await main(["trail", id, "--tester", "--business", "demo"], x, s.deps);
|
|
assert.match(x.out.text, /messages#\d+ {2}tester → coder: \[mos test\] hello/);
|
|
assert.deepEqual([...new Set(s.calls.tester.map((c) => c.verb))], ["message.send", "inbox", "messages", "tasks", "agents", "trail"]);
|
|
assert.deepEqual(s.calls.human, []);
|
|
// Without --tester the same commands refuse inside an agent, before any call.
|
|
for (const argv of [["inbox"], ["agents"], ["talk", "coder", "[mos test] hi"], ["follow"]])
|
|
await assert.rejects(main([...argv, "--business", "demo"], inAgent(), s.deps), (e) => e.exitCode === 3, argv[0]);
|
|
assert.deepEqual(s.calls.human, []);
|
|
});
|
|
|
|
test("decide, stop and launches take no --tester", async (t) => {
|
|
const s = setup(t);
|
|
for (const argv of [["decide", "d1", "yes", "--tester"], ["stop", "run-1", "--tester"], ["launches", "off", "--tester"], ["launches", "on", "--tester"], ["launches", "list", "--tester"]])
|
|
await assert.rejects(main(argv, inAgent(), s.deps), (e) => e.exitCode === 4 && /unknown option --tester/.test(e.message), argv.join(" "));
|
|
assert.deepEqual(s.calls, { human: [], tester: [] });
|
|
});
|
|
|
|
test("the tester transport runs tester-cli.mjs, and the tester's refusals exit 3", async () => {
|
|
assert.ok(TESTER_CLI.endsWith("/packages/bus/src/tester-cli.mjs") && existsSync(TESTER_CLI));
|
|
assert.notEqual(TESTER_CLI, HUMAN_CLI);
|
|
let seen;
|
|
const spawn = (cmd, argv, options) => ((seen = { cmd, argv, input: options.input }), { status: 0, stdout: "[]" });
|
|
assert.deepEqual(await testerTransport({ socket: "/run/fake.sock", business: "acme", env: {}, spawn })("agents"), []);
|
|
assert.deepEqual(seen.argv, [TESTER_CLI, "/run/fake.sock"]);
|
|
assert.deepEqual(JSON.parse(seen.input), { business: "acme", verb: "agents", args: {} });
|
|
for (const code of ["tester-required", "tester-off", "tester-refused"]) assert.equal(busExit(code), 3, code);
|
|
});
|
|
|
|
test("with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket", async (t) => {
|
|
const { f, org } = await orgFixture(t, "demo");
|
|
const dataRoot = f.dataRoot;
|
|
const seen = [];
|
|
const testerSpawn = (cmd, argv, options) => (seen.push({ argv, input: JSON.parse(options.input) }), { status: 0, stdout: "[]" });
|
|
await main(["agents", "--tester", "--business", "demo"], io({ ...f.env, CLAUDECODE: "1" }), { system: { dataRoot }, testerSpawn });
|
|
assert.deepEqual(seen, [{ argv: [TESTER_CLI, join(org.root, "bus", "broker.sock")], input: { business: "demo", verb: "agents", args: {} } }]);
|
|
});
|
|
|
|
// Row 277 (#1782, C1/C2): the human's text adds thread.claims, which a tester can't read, so
|
|
// `agents --tester` stays one call. --json prints the agents array alone, as `inbox --json` does.
|
|
test("agents --tester makes one call; the human's agents also reads thread.claims, and --json is the agents array", async (t) => {
|
|
const s = setup(t);
|
|
await main(["agents", "--tester", "--business", "demo"], inAgent(), s.deps);
|
|
assert.deepEqual(s.calls.tester.map((c) => c.verb), ["agents"]);
|
|
const x = io();
|
|
await main(["agents", "--business", "demo"], x, s.deps);
|
|
assert.deepEqual(s.calls.human.map((c) => c.verb), ["agents", "thread.claims"]);
|
|
assert.match(x.out.text, /^coder coder-run pi since /);
|
|
const j = io();
|
|
await main(["agents", "--json", "--business", "demo"], j, s.deps);
|
|
assert.deepEqual(s.calls.human.map((c) => c.verb), ["agents", "thread.claims", "agents"]);
|
|
assert.deepEqual(JSON.parse(j.out.text), s.b.request(s.human, { verb: "agents", args: {} }));
|
|
});
|
|
|
|
// Row 277 N1 (sage-2505): a refused thread.claims doesn't fail the human's agents. The role rows
|
|
// print, then the refusal with its code.
|
|
test("a refused thread.claims still prints the role rows and the refusal's code", async (t) => {
|
|
const s = setup(t);
|
|
const refusing = (business) => async (verb, args) => {
|
|
if (verb !== "thread.claims") return s.deps.transport(business)(verb, args);
|
|
const error = new Error("processing-halted");
|
|
error.code = "processing-halted";
|
|
throw error;
|
|
};
|
|
const x = io();
|
|
await main(["agents", "--business", "demo"], x, { ...s.deps, transport: refusing });
|
|
assert.match(x.out.text, /^coder coder-run pi since .+\nthread claims: refused processing-halted\n$/);
|
|
assert.deepEqual(s.calls.human.map((c) => c.verb), ["agents"]);
|
|
});
|
|
|
|
// Row 277 r4 (duckworth-0123 N1): an error with no bus code is not a refusal, so it still throws and
|
|
// nothing prints: no code at all, and a code that isn't the bus's shape.
|
|
test("a thread.claims error without a bus code still throws and prints nothing", async (t) => {
|
|
const s = setup(t);
|
|
for (const code of [undefined, "ECONNRESET"]) {
|
|
const error = new Error("socket closed");
|
|
if (code !== undefined) error.code = code;
|
|
const failing = (business) => async (verb, args) => {
|
|
if (verb !== "thread.claims") return s.deps.transport(business)(verb, args);
|
|
throw error;
|
|
};
|
|
const x = io();
|
|
await assert.rejects(main(["agents", "--business", "demo"], x, { ...s.deps, transport: failing }), (e) => e === error, String(code));
|
|
assert.equal(x.out.text, "", String(code));
|
|
}
|
|
});
|
|
|
|
// Row 170 (#1667): the bus refuses a tester's message to a stopped role and stores nothing, and
|
|
// talk says so and exits 3.
|
|
test("talk --tester to a stopped role exits 3, prints no sent line and stores nothing", async (t) => {
|
|
const s = setup(t, { launch: { by: "pm", instances: ["cto", "coder"], models: {}, limits: {} } });
|
|
const x = inAgent();
|
|
await assert.rejects(main(["talk", "cto", "[mos test] hello", "--tester", "--wait", "0", "--business", "demo"], x, s.deps), (e) => {
|
|
assert.equal(e.exitCode, 3);
|
|
assert.equal(e.message, "cto isn't running, so the tester's message was refused and nothing was stored");
|
|
return true;
|
|
});
|
|
assert.equal(x.out.text, "");
|
|
assert.equal(s.store.get("SELECT count(*) n FROM messages").n, 0);
|
|
assert.deepEqual(s.calls.tester.map((c) => c.verb), ["message.send"]);
|
|
});
|
|
|
|
test("only a --tester send refused tester-target-not-running is rewritten", async (t) => {
|
|
const s = setup(t);
|
|
const refusing = (code) => () => async (verb) => {
|
|
const error = new Error(code);
|
|
error.code = code;
|
|
error.exitCode = 3;
|
|
throw error;
|
|
};
|
|
// Another refusal of a tester send keeps the bus's error.
|
|
for (const code of ["tester-off", "tester-required", "tester-refused", "unknown-role"]) {
|
|
const x = inAgent();
|
|
await assert.rejects(main(["talk", "cto", "[mos test] hello", "--tester", "--wait", "0", "--business", "demo"], x, { ...s.deps, testerTransport: refusing(code) }), (e) => {
|
|
assert.equal(e.code, code);
|
|
assert.equal(e.message, code);
|
|
return true;
|
|
}, code);
|
|
assert.equal(x.out.text, "");
|
|
}
|
|
// The human's send never gets the tester's line, whatever its transport says.
|
|
await assert.rejects(main(["talk", "cto", "hello", "--wait", "0", "--business", "demo"], io(humanEnv()), { ...s.deps, transport: refusing("tester-target-not-running") }), (e) => {
|
|
assert.equal(e.code, "tester-target-not-running");
|
|
assert.equal(e.message, "tester-target-not-running");
|
|
return true;
|
|
});
|
|
});
|