Files
stack/packages/business/tests/cli.test.mjs
T
jason.woltjeandClaude Opus 5.5 94e41ae29e roles: add the assistant role, move role.launch from the PM to the chief of staff (row 119, #1613)
roles/assistant.json and assistant.md are new. The assistant holds only
message.send and relays each ruling verbatim to the chief of staff. The
chief of staff gains role.launch; the PM drops it. Test fixtures move
launch.by from "pm" to a `chief` instance.

This does not change the live host. Jason's organization file names
launch.by "pm", which the new role files refuse, so his edit and the
re-pin happen together in one Mos window (decision 144; the order is in
agents/researcher/work/queue-119/build.md).

Lands Researcher's round 2 candidate, approved by Filbert in comment
28528 (rev 1240). Candidate manifest f77c4917, all 22 files match.
Landing gate on 46117feb8: 23 suites, GATE_PASSED 01:09:35Z.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 20:10:11 -05:00

162 lines
7.2 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { projectFilePath } from "../src/index.mjs";
import { businessDoc, REPO, rolesCopy, systemConfig, tmp, tokenFile, writeJson } from "./helpers.mjs";
const CLI = join(REPO, "packages", "business", "src", "cli.mjs");
// A complete scratch setup: system config, business file, roles copy.
// HOME points into the scratch tree too, so nothing reads the real one.
function setup(mutate) {
const root = tmp();
const config = systemConfig(root);
const doc = businessDoc(root);
mutate?.(doc, root);
writeJson(join(root, "config", "businesses", `${doc.id}.json`), doc);
const env = { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: rolesCopy(root) };
return { root, doc, env };
}
function run(s, ...args) {
const proc = spawnSync(process.execPath, [CLI, ...args], { env: s.env, encoding: "utf8" });
return { code: proc.status, out: proc.stdout, err: proc.stderr };
}
test("usage errors exit 4", () => {
const s = setup();
assert.equal(run(s).code, 4);
assert.equal(run(s, "show", "acme").code, 4);
assert.equal(run(s, "validate").code, 4);
assert.equal(run(s, "validate", "acme", "extra").code, 4);
assert.equal(run(s, "resolve", "acme").code, 4);
assert.equal(run(s, "resolve", "acme", "pm", "--project").code, 4);
assert.equal(run(s, "resolve", "acme", "pm", "--project", "a", "--project", "b").code, 4);
assert.match(run(s).err, /usage: mosaic business validate/);
});
test("validate: a good business exits 0 and prints instance digests", () => {
const s = setup();
const r = run(s, "validate", "acme");
assert.equal(r.code, 0, r.err);
const out = JSON.parse(r.out);
assert.equal(out.business, "acme");
assert.deepEqual(out.projects, { stack: "absent" });
assert.deepEqual(Object.keys(out.instances), ["pm", "cto", "coder", "reviewer", "chief"]);
for (const digest of Object.values(out.instances)) assert.match(digest, /^[0-9a-f]{64}$/);
assert.match(r.err, /warning: project stack: no project file/);
assert.doesNotMatch(r.out + r.err, /placeholder-not-a-token/);
});
test("validate: project files", () => {
const s = setup();
const file = projectFilePath(s.doc.projects.stack.root);
writeJson(file, { projectVersion: 1, id: "stack", vars: { "tracker.project": 3 } });
const r = run(s, "validate", "acme");
assert.equal(r.code, 0, r.err);
assert.deepEqual(JSON.parse(r.out).projects, { stack: "valid" });
writeJson(file, { projectVersion: 1, id: "other" });
assert.match(run(s, "validate", "acme").err, /has id other, but business acme declares it as stack/);
assert.equal(run(s, "validate", "acme").code, 2);
writeJson(file, { projectVersion: 1, id: "stack", roles: { ghost: {} } });
const ghost = run(s, "validate", "acme");
assert.equal(ghost.code, 2);
assert.match(ghost.err, /role instance ghost/);
writeJson(file, { projectVersion: 1, id: "stack", vars: { harness: "pi" } });
assert.equal(run(s, "validate", "acme").code, 2);
});
test("validate: missing files and a broken system config", () => {
const s = setup();
const missing = run(s, "validate", "nobody");
assert.equal(missing.code, 4);
assert.match(missing.err, /business file not found/);
assert.equal(run(s, "validate", "Bad!").code, 2);
const noConfig = { ...s, env: { ...s.env, MOSAIC_CONFIG: join(s.root, "absent", "config.json") } };
const r = run(noConfig, "validate", "acme");
assert.equal(r.code, 3);
assert.match(r.err, /system config problem/);
writeFileSync(s.env.MOSAIC_CONFIG, "{");
assert.equal(run(s, "validate", "acme").code, 3);
});
test("validate: credential reference problems exit 2 and name each one", () => {
const s = setup((doc, root) => {
chmodSync(doc.roles.cto.credentials.gitea.file, 0o644);
doc.roles.coder.credentials.vikunja.file = tokenFile(join(root, "data", "tokens"), "coder.token");
doc.roles.reviewer.credentials.vikunja.expires = "2026-01-01";
});
const r = run(s, "validate", "acme");
assert.equal(r.code, 2);
assert.equal(r.out, "");
assert.match(r.err, /cto-gitea\.token: mode 644/);
assert.match(r.err, /coder\.token: inside .*data/);
assert.match(r.err, /expired on 2026-01-01/);
assert.match(r.err, /3 credential reference problem/);
});
test("validate: a token file inside the repository is refused", () => {
const s = setup((doc) => { doc.roles.pm.credentials.gitea.file = join(REPO, "roles", "pm.md"); });
const r = run(s, "validate", "acme");
assert.equal(r.code, 2);
assert.match(r.err, /inside .*; token files live outside the repository and dataRoot/);
});
test("validate: role definitions come from MOSAIC_ROLES_DIR", () => {
const s = setup();
const before = JSON.parse(run(s, "validate", "acme").out).instances;
const pmFile = join(s.env.MOSAIC_ROLES_DIR, "pm.json");
const pm = JSON.parse(readFileSync(pmFile, "utf8"));
pm.tools = pm.tools.filter((t) => t !== "write");
writeJson(pmFile, pm);
const after = JSON.parse(run(s, "validate", "acme").out).instances;
assert.notEqual(after.pm, before.pm);
assert.equal(after.cto, before.cto);
pm.authority.withinRole.push("deploy");
writeJson(pmFile, pm);
const gated = run(s, "validate", "acme");
assert.equal(gated.code, 2);
assert.match(gated.err, /always gated/);
});
test("resolve: prints one instance's record", () => {
const s = setup();
writeJson(projectFilePath(s.doc.projects.stack.root), {
projectVersion: 1, id: "stack", vars: { "tracker.project": 3 }, roles: { coder: { vars: { "limits.tools": ["read", "bash"] } } },
});
const r = run(s, "resolve", "acme", "coder", "--project", "stack");
assert.equal(r.code, 0, r.err);
const coder = JSON.parse(r.out);
assert.equal(coder.project, "stack");
assert.equal(coder.vars["tracker.project"], 3);
assert.deepEqual(coder.limits.tools, ["read", "bash"]);
assert.equal(coder.limits.network, "none");
assert.equal(coder.contract, join(s.env.MOSAIC_ROLES_DIR, "coder.md"));
assert.doesNotMatch(r.out, /placeholder-not-a-token/);
const chief = JSON.parse(run(s, "resolve", "acme", "chief").out);
assert.equal(chief.project, null);
assert.ok(chief.limits.authority.withinRole.includes("role.launch"));
const pm = JSON.parse(run(s, "resolve", "acme", "pm").out);
assert.ok(!pm.limits.authority.withinRole.includes("role.launch"));
const validated = JSON.parse(run(s, "validate", "acme").out).instances;
assert.equal(chief.digest, validated.chief);
});
test("resolve: refusals", () => {
const s = setup((doc) => { chmodSync(doc.roles.reviewer.credentials.gitea.file, 0o604); });
assert.equal(run(s, "resolve", "acme", "pm").code, 0);
const reviewer = run(s, "resolve", "acme", "reviewer");
assert.equal(reviewer.code, 2);
assert.equal(reviewer.out, "");
assert.match(reviewer.err, /mode 604/);
assert.equal(run(s, "resolve", "acme", "ghost").code, 2);
assert.equal(run(s, "resolve", "acme", "constructor").code, 2);
assert.match(run(s, "resolve", "acme", "pm", "--project", "ghost").err, /declares no project "ghost"/);
assert.equal(run(s, "resolve", "acme", "pm", "--project", "stack").code, 4);
});