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 } 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) { const store = new Store(tmp(t, "mosaic-cli-tester-")); t.after(() => store.close()); const businesses = { demo: { ...BUSINESSES.demo, vars: { "bus.tester": true } } }; 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 [mos test] 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", "[mos test] 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); }); test("talk --tester refuses text without the [mos test] prefix before any call", async (t) => { const s = setup(t); await assert.rejects(main(["talk", "coder", "is the plan current?", "--tester", "--business", "demo"], inAgent(), s.deps), (e) => e.exitCode === 2 && /starts with "\[mos test\]"/.test(e.message)); assert.deepEqual(s.calls.tester, []); // follow sends nothing, so it takes no prefix. 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); assert.equal(busExit("tester-prefix"), 2); }); test("with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket", async (t) => { const dataRoot = tmp(t); const seen = []; const testerSpawn = (cmd, argv, options) => (seen.push({ argv, input: JSON.parse(options.input) }), { status: 0, stdout: "[]" }); await main(["agents", "--tester", "--business", "demo"], inAgent(), { system: { dataRoot }, testerSpawn }); assert.deepEqual(seen, [{ argv: [TESTER_CLI, join(dataRoot, "bus", "broker.sock")], input: { business: "demo", verb: "agents", args: {} } }]); });