// Row 175 (#1673, Q173, mos-0870 N2, mos-0872): the CLI's tester forms. The reads print what the // human's forms print (since row 177 `threads --tester` lists the tester's own), every write form // outside W1-W7 refuses --tester before any call, and `project init --tester` checks the project // area, binds the tester, then commits as `tester`. import { test } from "node:test"; import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; import { existsSync, lstatSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { main } from "../src/cli.mjs"; import { BusError } from "../../bus/src/broker.mjs"; import { startBroker } from "../../bus/src/runtime.mjs"; import { systemFor, writeJson } from "../../organization/tests/helpers.mjs"; import { fixture, humanEnv, io, tmp, orgFixture } from "./helpers.mjs"; const none = { authority: { withinRole: ["message.send"], crossRole: [] } }; const inAgent = (env) => io({ ...env, CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "sdk-ts" }); const bus = (vars = { "bus.tester": true }) => ({ acme: { id: "acme", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' }, ...(vars ? { vars } : {}), roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: "coder", ...none }, assistant: { definition: "assistant", ...none } }, }, }); // The runtime in this process behind stand-in transports, as instances.test.mjs. async function setup(t) { const root = tmp(t, "mosaic-cli-tester-reads-"); const f = fixture(root, "acme"); const rt = await startBroker({ dataRoot: f.dataRoot, businesses: bus(), organization: { dir: join(root, "config"), rolesDir: f.roles, system: systemFor(root) } }); t.after(() => rt.close()); const human = rt.broker.bindHuman({ business: "acme", human: "jason", via: "cli", outsideAgent: true }); const tester = rt.broker.bindTester({ business: "acme" }); const calls = { human: [], tester: [] }; const via = (cap, list) => (business) => async (verb, args = {}) => { list.push(verb); try { return structuredClone(rt.broker.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: f.dataRoot }, transport: via(human, calls.human), testerTransport: via(tester, calls.tester), talkPollMs: 5 }; const call = (verb, args = {}) => rt.broker.request(human, { verb, args }); return { root, f, rt, human, tester, calls, deps, call }; } const READS = [ ["usage"], ["names"], ["limits"], ["plugins"], // Row 177 (#1675): `threads` left this list; the tester's lists its own threads (below). ["project", "list"], ]; test("row 175: each read with --tester inside an agent prints the human form's output, and refuses without --tester", async (t) => { const s = await setup(t); s.call("plugins.set", { revision: s.call("plugins").revision, plugins: { channel: ["cli", "webui"] } }); s.call("name.set", { instance: "coder", name: "rex" }); const project = s.call("project.create", { name: "alpha" }).id; s.call("thread.create", { project, name: "one" }); const forms = [...READS.map((argv) => [...argv, "--business", "acme"]), ["business", "instances", "acme"]]; for (const argv of forms) { const name = argv.slice(0, 2).join(" "); const h = io(s.f.env); await main(argv, h, s.deps); const humanCalls = s.calls.human.length; const testerCalls = s.calls.tester.length; const x = inAgent(s.f.env); await main([...argv, "--tester"], x, s.deps); assert.equal(x.out.text, h.out.text, name); assert.ok(x.out.text.length > 0, name); assert.equal(s.calls.human.length, humanCalls, `${name}: the tester form made no human call`); assert.ok(s.calls.tester.length > testerCalls, `${name}: the tester form called the tester transport`); await assert.rejects(main(argv, inAgent(s.f.env), s.deps), (e) => e.exitCode === 3, `${name} without --tester`); assert.equal(s.calls.human.length, humanCalls, `${name}: the refusal came before any call`); } // Row 177 (#1675): the tester's threads are its own, so the human's "one" is not in its list. const testerThreads = async () => { const x = inAgent(s.f.env); await main(["threads", "--business", "acme", "--json", "--tester"], x, s.deps); return JSON.parse(x.out.text).map((r) => r.name); }; assert.deepEqual(await testerThreads(), []); s.rt.broker.request(s.tester, { verb: "thread.create", args: { project, name: "t-one" } }); assert.deepEqual(await testerThreads(), ["t-one"]); const h = io(s.f.env); await main(["threads", "--business", "acme", "--json"], h, s.deps); assert.deepEqual(JSON.parse(h.out.text).map((r) => r.name), ["one"]); await assert.rejects(main(["threads", "--business", "acme"], inAgent(s.f.env), s.deps), (e) => e.exitCode === 3, "threads without --tester"); }); test("row 175: --tester on limits set, reset and migrate and every write form outside W1-W7 exits 4 before any call", async (t) => { const s = await setup(t); for (const argv of [ ["limits", "set", "--file", "patch.json", "--business", "acme"], ["limits", "reset", "total", "--business", "acme"], ["limits", "migrate", "--file", "org.json", "--models", "map.json"], ["decide", "d1", "yes", "--business", "acme"], ["inbox", "--read", "abcdefgh", "--business", "acme"], ["inbox", "--read-all", "--business", "acme"], // Row 184 (#1682): the thread verbs and talk and follow --thread left this list // (tests/tester-threads.test.mjs). ["stop", "run-1"], ["launches", "off", "--business", "acme"], ["launches", "on", "--business", "acme"], ["launches", "list"], ["bus", "start", "acme"], ["bus", "stop"], ["project", "check", "acme", "stack"], ["project", "init", "acme", "stack", "--tester"], ]) { const x = inAgent(s.f.env); await assert.rejects(main([...argv, "--tester"], x, s.deps), (e) => e.exitCode === 4, argv.join(" ")); } assert.deepEqual(s.calls, { human: [], tester: [] }); assert.equal(existsSync(join(s.f.dataRoot, "projects")), false); }); // W7 through the real tester transport: the CLI runs as its own process, so the tester-cli.mjs it // spawns can reach the runtime in this one. const MOSAIC = fileURLToPath(new URL("../../../scripts/mosaic", import.meta.url)); function command(executable, args, env) { const clean = Object.fromEntries(Object.entries(env).filter(([k]) => !/^MOSAIC_BUS_(TESTER|CLI)_NONCE$/.test(k))); return new Promise((resolve, reject) => { const child = spawn(executable, args, { env: clean, timeout: 60000 }); let out = ""; let err = ""; child.stdout.on("data", (d) => (out += d)); child.stderr.on("data", (d) => (err += d)); child.on("error", reject); child.on("close", (status) => resolve({ status, out, err })); }); } const mosaic = (args, env) => command(MOSAIC, args, env); // sage-1081: these six fell through to the seat CLI, which refused with its own usage, so W1 // (`plugins set`) and W4 (`name`) failed through the wrapper. Each now reaches packages/cli. test("row 175: scripts/mosaic sends plugins, name, names, follow, threads and thread to packages/cli, not the seat CLI", (t) => { const root = tmp(t, "mosaic-cli-route-"); const env = { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: join(root, "none.json") }; for (const [args, message] of [ [["plugins", "x"], /^mosaic: mosaic plugins \[--business ID\]/], [["name"], /^mosaic: name takes an instance and a name\n/], [["names", "x"], /^mosaic: names takes no arguments\n/], [["follow", "x"], /^mosaic: follow takes no arguments\n/], [["threads", "x"], /^mosaic: threads takes no arguments\n/], [["thread"], /^mosaic: usage:\n mosaic thread create /], ]) { const r = spawnSync(MOSAIC, args, { env, encoding: "utf8" }); assert.equal(r.status, 4, args.join(" ")); assert.match(r.stderr, message, args.join(" ")); assert.doesNotMatch(r.stderr, /mosaic launch readdirSync(dir, { recursive: true }).map(String).sort(); // Names, types and file bytes: a rewrite of an existing file shows up, not just a new path. const contents = (dir) => listing(dir).map((name) => { const path = join(dir, name), st = lstatSync(path); return [name, st.isFile() ? readFileSync(path).toString("base64") : st.isDirectory() ? "dir" : `mode ${st.mode}`]; }); async function w7(t, vars, edit) { const root = tmp(t, "w7-"); const f = fixture(root, "acme", edit); const rt = await startBroker({ dataRoot: f.dataRoot, businesses: bus(vars) }); t.after(() => rt.close()); // Production CLI routes its transport through an organization. Inject the real tester transport // in a child so the legacy non-org project path can still exercise the broker and its proof. const driver = join(root, "driver.mjs"); writeFileSync(driver, ` import { run } from ${JSON.stringify(new URL("../src/cli.mjs", import.meta.url).href)}; import { testerTransport } from ${JSON.stringify(new URL("../src/transport.mjs", import.meta.url).href)}; const [socket, ...args] = process.argv.slice(2); process.exitCode = await run(args, undefined, { testerTransport: business => testerTransport({ socket, business }), }); `); const agent = { ...f.env, CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "sdk-ts" }; const project = (args, env = agent) => command(process.execPath, [driver, join(f.dataRoot, "bus", "broker.sock"), ...args], env); return { root, f, agent, project }; } test("row 175 W7: project init --tester makes the repository under the project area, committed by tester", async (t) => { const { f, agent, project } = await w7(t); const r = await project(["project", "init", "acme", "stack", "--tester"], agent); assert.equal(r.status, 0, r.err); const repo = join(f.dataRoot, "projects", "acme", "stack"); assert.match(r.out, new RegExp(`^initialized ${repo}\ncommit [0-9a-f]{40}\n$`)); const log = spawnSync("git", ["-C", repo, "log", "--format=%an|%cn"], { encoding: "utf8" }); assert.equal(log.stdout, "tester|tester\n"); // Without --tester the same init inside an agent refuses, as before. const again = await project(["project", "init", "acme", "stack"], agent); assert.equal(again.status, 3); assert.match(again.err, /outside any agent run/); }); test("row 175 W7: a repo outside the project area refuses tester-project-area before any bus call, and nothing changes", async (t) => { // The switch is off: a bind would refuse tester-off (exit 3), so exit 2 shows the area check ran first. const { f, agent, project } = await w7(t, null, (doc) => { doc.projects.stack.repo = join(doc.projects.stack.root, "..", "records"); return doc; }); const records = f.doc.projects.stack.repo; const data = listing(f.dataRoot); const r = await project(["project", "init", "acme", "stack", "--tester"], agent); assert.equal(r.status, 2, r.err); assert.match(r.err, /tester-project-area/); assert.equal(existsSync(records), false); assert.deepEqual(listing(f.dataRoot), data); }); test("row 175 W7: the tester bind refuses tester-off and tester-required before any directory", async (t) => { const off = await w7(t, null); const data = listing(off.f.dataRoot); const r = await off.project(["project", "init", "acme", "stack", "--tester"], off.agent); assert.equal(r.status, 3, r.err); assert.match(r.err, /tester-off/); assert.deepEqual(listing(off.f.dataRoot), data); assert.equal(existsSync(join(off.f.dataRoot, "projects")), false); const on = await w7(t); const before = listing(on.f.dataRoot); for (const marker of ["MOSAIC_RUN_ID", "MOSAIC_BUS_CAP", "MOSAIC_AGENT_RUN"]) { const under = await on.project(["project", "init", "acme", "stack", "--tester"], { ...on.agent, [marker]: "x" }); assert.equal(under.status, 3, `${marker}: ${under.err}`); assert.match(under.err, /tester-required/, marker); } assert.deepEqual(listing(on.f.dataRoot), before); assert.equal(existsSync(join(on.f.dataRoot, "projects")), false); }); // Organization setup deliberately refuses tester init before creating a project record. test("organization project init --tester refuses without writing", async (t) => { const { f, org } = await orgFixture(t); const before = contents(org.root); const agent = { ...f.env, CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "sdk-ts" }; const r = await mosaic(["project", "init", "acme", "stack", "--tester"], agent); assert.equal(r.status, 3, r.err); assert.match(r.err, /organization project init --tester is unavailable/); assert.deepEqual(contents(org.root), before); });