// Shared fixtures. Every test works in its own temporary directory and // points MOSAIC_CONFIG there; nothing reads the real ~/.config, a real // token, a real binding, or the real human transport. import { chmodSync, cpSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { PassThrough } from "node:stream"; import { pathToFileURL } from "node:url"; import { BusError, Broker } from "../../bus/src/broker.mjs"; import { Store } from "../../bus/src/store.mjs"; import { businessDoc, rolesCopy, systemConfig, writeJson } from "../../organization/tests/helpers.mjs"; import { REPO_SKILLS } from "../../harness/tests/helpers.mjs"; import { tree } from "../../plugins/tests/helpers.mjs"; import { SESSION_CREDENTIALS } from "../../seat/src/session.mjs"; import { AGENT_MARKERS } from "../src/transport.mjs"; import { reserveOrg, activateOrg } from "../../organization/src/org-registry.mjs"; // Row 332 R2 (#1830): every host variable a launch reads as a login. The // session login names come from the seat; the other three are what pi's // preflight also takes as its own provider key. A test that deletes only the // session names passes or fails by what the host happens to export. export const HOST_CREDENTIALS = Object.freeze([...new Set([ ...SESSION_CREDENTIALS.flat(), "OPENAI_API_KEY", "OPENROUTER_API_KEY", "ZAI_API_KEY", ])]); // A made-up pi provider key: obviously fake, never a real value. export const FAKE_PI_KEY = "fx332-fake-pi"; // Remove every host credential from `env`, then, for a test that launches pi, // set the one fake pi key. Returns `env`. export function withoutHostCredentials(env, { piKey = false } = {}) { for (const k of HOST_CREDENTIALS) delete env[k]; if (piKey) env.ZAI_API_KEY = FAKE_PI_KEY; return env; } export function tmp(t, prefix = "mosaic-cli-") { const root = mkdtempSync(join(tmpdir(), prefix)); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } // The environment with every agent marker removed: the test runner itself // may run under an agent, and the commands refuse there. export function humanEnv(extra = {}) { const env = { ...process.env, ...extra }; for (const k of AGENT_MARKERS) delete env[k]; delete env.NODE_TEST_CONTEXT; return env; } // A config directory with the system config, the roles and business `id`. Row 65: both harnesses // are switched on, since a launch on one that isn't refuses harness-off, and every instance names a // harness, model and thinking level, as the live file does, since a launch missing one refuses // no-route. The instance's own vars win, and edit can change any of it. Row 64: the data root's // skills-enabled holds the repository's skills, since a launch whose role names one that isn't // there refuses skill-not-enabled. The fixture makes the data root (0700) to hold them. export function fixture(root, id = "acme", edit = (doc) => doc) { const config = systemConfig(root); const roles = rolesCopy(root); const base = businessDoc(root, id); for (const r of Object.values(base.roles)) r.vars = { harness: "pi", model: "glm-5.3-flash", thinking: "high", ...r.vars }; const doc = edit({ ...base, plugins: { ...base.plugins, harness: ["pi", "claude-code"] } }); writeJson(join(root, "config", "businesses", `${id}.json`), doc); mkdirSync(join(root, "data"), { recursive: true, mode: 0o700 }); cpSync(REPO_SKILLS, join(root, "data", "skills-enabled"), { recursive: true }); return { config, roles, dataRoot: join(root, "data"), doc, env: humanEnv({ MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: roles }) }; } // An active, named organization for real transport fixtures. The business // document remains the fixture's original document; the bus root is isolated. export async function orgFixture(t, id = "acme", edit = doc => doc) { const root = tmp(t, "o-"), f = fixture(root, id, edit); mkdirSync(join(root, ".config"), { mode: 0o700 }); const dir = join(root, ".config", "mosaic-test"); renameSync(join(root, "config"), dir); chmodSync(dir, 0o700); writeFileSync(join(f.dataRoot, "instance.json"), JSON.stringify({ instanceVersion: 1, name: "test" })); f.config = join(dir, "config.json"); f.env.MOSAIC_CONFIG = f.config; f.env.HOME = root; const ctx = { configDir: dir, dataRoot: f.dataRoot, instance: "test" }; const org = await reserveOrg(ctx, id); await activateOrg(ctx, id); return { root, f, org }; } export function notifyConfig(dataRoot, business, binding) { // The journal sits beside notify.json and refuses a directory looser than 0700. mkdirSync(join(dataRoot, "notify", business), { recursive: true, mode: 0o700 }); return writeJson(join(dataRoot, "notify", business, "notify.json"), { notifyVersion: 1, binding }); } // An io for main(): captured stdout and stderr, a stdin that is not a TTY. export function io(env = humanEnv()) { const out = { text: "" }; const err = { text: "" }; const stdin = new PassThrough(); stdin.isTTY = false; return { env, stdin, stdout: { write: (s) => ((out.text += s), true) }, stderr: { write: (s) => ((err.text += s), true) }, out, err, }; } export const OPTIONS = [ { key: "yes", text: "Allow" }, { key: "no", text: "Decline" }, ]; export const BUSINESSES = { demo: { id: "demo", human: "jason", plugins: { harness: ["pi"] }, arbiters: { technical: "cto", delivery: "pm" }, roles: { pm: { authority: { withinRole: ["message.send"], crossRole: [] } }, cto: { authority: { withinRole: ["message.send"], crossRole: [] } }, coder: { authority: { withinRole: ["message.send"], crossRole: ["task.scope.change"] } }, }, }, }; // An in-process broker with a claimed coder and the human's capability. // transport(business) mirrors humanTransport: errors carry the bus code. export function broker(t, businesses = BUSINESSES) { const root = tmp(t, "mosaic-cli-bus-"); const store = new Store(root); t.after(() => store.close()); 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 reader = b.bindReader({ business: "demo" }); const calls = []; const transport = (business) => async (verb, args = {}) => { calls.push({ business, verb, args }); try { return structuredClone(b.request(human, { verb, args })); } catch (e) { if (!(e instanceof BusError)) throw e; const error = new Error(e.code); error.code = e.code; throw error; } }; const raise = (action, extra = {}) => b.request(coder, { verb: "decision.raise", args: { action, question: "Push the release?", options: OPTIONS, recommendation: "no", blocking: false, ...extra } }); return { b, store, coder, human, reader, transport, calls, raise, read: (verb, args = {}) => structuredClone(b.request(reader, { verb, args })) }; } // A script standing in for packages/bus/src/human-cli.mjs: it reads one // request on stdin and answers from the table in its first argument. export function fakeCli(root, body) { const file = join(root, "fake-human-cli.mjs"); writeFileSync(file, body); chmodSync(file, 0o600); return file; } // Row 65: a launcher's makeLoader over a scratch plug-in tree. adapters maps a harness module to the // adapter script a test runs in its place, or to null for the real module unchanged. A faked module // re-exports the real one with only adapter() replaced; a faked claude-code also passes a missing // claude binary, since the fake adapter never runs one (packages/plugins/tests/harness.test.mjs // covers that check). more(write) adds fixture modules, write(kind, name, body). export async function fakeHarnesses(t, adapters, more = () => {}) { const { write, loaderUrl } = tree(t); for (const [name, adapter] of Object.entries(adapters)) { const real = JSON.stringify(pathToFileURL(new URL(`../../plugins/harness/${name}/index.mjs`, import.meta.url).pathname).href); if (adapter === null) { write("harness", name, `export * from ${real};\n`); continue; } const lines = [`export * from ${real};`, `export const adapter = () => ${JSON.stringify(adapter)};`]; if (name === "claude-code") { lines.push(`import { preflight as real } from ${real};`, "export function preflight(input) { const p = real(input); return p?.code === \"harness-binary-missing\" ? null : p; }"); } write("harness", name, lines.join("\n") + "\n"); } more(write); const { createLoader } = await import(loaderUrl); return createLoader; }