Files
stack/packages/business/tests/cli.test.mjs
T
jason.woltjeandClaude Opus 5.5 2d64c71eb2 feat(business): roles v2, business and project files, variable layers (row 36, S1, darkwing)
Darkwing's round 2 candidate, approved by Filbert (#1518 comment 26730).
build-r2.patch a27890d5, manifest 869168c7, 34 files, applied on HEAD and
checked 34/34. Integration gate on an export of HEAD plus the patch:
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Conductor, queue, conversation and discord confirmed in git worktrees of
HEAD with and without the patch, identical results. Lead decision 63
accepts the vocabulary location, the example path and the business
branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:09:07 -05:00

160 lines
7.0 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"]);
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 pm = JSON.parse(run(s, "resolve", "acme", "pm").out);
assert.equal(pm.project, null);
assert.ok(pm.limits.authority.withinRole.includes("role.launch"));
const validated = JSON.parse(run(s, "validate", "acme").out).instances;
assert.equal(pm.digest, validated.pm);
});
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);
});