Files
stack/packages/business/tests/business.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

221 lines
13 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { chmodSync, lstatSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { BusinessError, businessFilePath, configDir, loadBusiness, validateBusinessDocument } from "../src/index.mjs";
import { businessDoc, REPO, REPO_ROLES, rolesCopy, tmp, writeJson } from "./helpers.mjs";
function refuses(fn, pattern, exitCode = 2) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.equal(error.exitCode, exitCode);
assert.match(error.message, pattern);
return true;
});
}
// Validate a mutated copy of the fixture document.
function check(mutate, { rolesDir = REPO_ROLES } = {}) {
const root = tmp();
const doc = businessDoc(root);
mutate?.(doc, root);
return validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir });
}
test("config directory and file path follow MOSAIC_CONFIG", () => {
assert.equal(configDir({ MOSAIC_CONFIG: "/x/cfg/config.json" }), "/x/cfg");
assert.match(configDir({}), /\.config\/mosaic-dev$/);
assert.equal(businessFilePath("acme", "/x/cfg"), "/x/cfg/businesses/acme.json");
refuses(() => businessFilePath("../acme", "/x"), /business id/);
});
test("the fixture business validates and comes back frozen", () => {
const business = check();
assert.equal(business.id, "acme");
assert.deepEqual(Object.keys(business.roles), ["pm", "cto", "coder", "reviewer"]);
assert.equal(business.roles.pm.holder, "sage");
assert.equal(business.roles.reviewer.holder, null);
assert.equal(business.roles.coder.vars["limits.network"], "none");
assert.equal(business.definitions.pm.roleVersion, 2);
assert.deepEqual(business.launch, { by: "pm", instances: ["coder", "reviewer"], max: { opus: 2, sonnet: 4 } });
assert.ok(Object.isFrozen(business.roles.pm.credentials.gitea));
assert.throws(() => { business.roles.pm.holder = "x"; }, TypeError);
});
test("two instances may share a definition", () => {
const business = check((doc) => {
doc.roles.coder2 = { ...structuredClone(doc.roles.coder), tracker: { bot: "bot-acme-coder2", botId: 7 } };
});
assert.equal(business.roles.coder2.definition, "coder");
});
test("top-level refusals", () => {
refuses(() => check((d) => { d.businessVersion = 2; }), /businessVersion/);
refuses(() => check((d) => { d.owner = "x"; }), /unsupported business file key: "owner"/);
refuses(() => check((d) => { d.id = "other"; }), /must match its filename/);
refuses(() => check((d) => { d.id = "Acme"; }), /business id/);
for (const key of ["human", "arbiters", "projects", "tracker", "roles"]) {
refuses(() => check((d) => { delete d[key]; }), new RegExp(`requires "${key}"`));
}
refuses(() => check((d) => { d.vars.harness = "pi"; }), /harness can't be set at the business layer/);
refuses(() => check((d) => { d.vars.secret = "x"; }), /unknown variable "secret"/);
});
test("arbiters and projects", () => {
refuses(() => check((d) => { d.arbiters.technical = "ghost"; }), /arbiters\.technical names ghost/);
refuses(() => check((d) => { delete d.arbiters.delivery; }), /arbiters\.delivery/);
refuses(() => check((d) => { d.arbiters.final = "pm"; }), /unsupported .* arbiters key/);
refuses(() => check((d) => { d.projects = {}; }), /at least one project/);
refuses(() => check((d) => { d.projects.stack.root = "relative/path"; }), /normalized absolute path/);
refuses(() => check((d) => { d.projects.stack.root += "/../x"; }), /normalized absolute path/);
refuses(() => check((d) => { d.projects.stack.branch = "main"; }), /unsupported .* key: "branch"/);
});
test("role instances", () => {
refuses(() => check((d) => { d.roles = {}; }), /at least one role instance/);
refuses(() => check((d) => { d.roles.pm.definition = "ghost"; }), /not found/, 4);
refuses(() => check((d) => { d.roles.pm.definition = "researcher"; }), /version 1 and can't back/);
refuses(() => check((d) => { d.roles.pm.token = "x"; }), /unsupported .*roles\.pm key: "token"/);
refuses(() => check((d) => { d.roles.pm.holder = "Sage!"; }), /holder/);
refuses(() => check((d) => { d.roles.pm.vars["tracker.baseUrl"] = "http://x"; }), /can't be set at the agent layer/);
refuses(() => check((d) => { d.roles.pm.vars["limits.authority"] = ["deploy.prod"]; }), /unknown action/);
});
test("Vikunja bots", () => {
refuses(() => check((d) => { delete d.roles.pm.tracker; }), /needs "tracker"/);
refuses(() => check((d) => { d.roles.pm.tracker.botId = 0; }), /botId must be a positive integer/);
refuses(() => check((d) => { d.roles.pm.tracker.bot = "pm-bot"; }), /starting with "bot-"/);
refuses(() => check((d) => { d.roles.cto.tracker.bot = d.roles.pm.tracker.bot; }), /used by another instance/);
refuses(() => check((d) => { d.roles.cto.tracker.botId = d.roles.pm.tracker.botId; }), /botId 3 is used by another instance/);
refuses(() => check((d) => { d.tracker.sync.bot = d.roles.pm.tracker.bot; }), /sync must use its own bot/);
refuses(() => check((d) => { d.tracker.sync.botId = d.roles.cto.tracker.botId; }), /sync must use its own bot/);
refuses(() => check((d) => { d.tracker.sync.botId = 0; }), /sync\.botId/);
refuses(() => check((d) => { d.tracker.labels = { "needs-jason": 0 }; }), /labels\.needs-jason/);
refuses(() => check((d) => { d.tracker.webhook = "x"; }), /unsupported .* tracker key/);
assert.deepEqual(check((d) => { d.tracker.labels = { "needs-jason": 12 }; }).tracker.labels, { "needs-jason": 12 });
});
test("a role without Vikunja takes no tracker block", () => {
const root = tmp();
const rolesDir = rolesCopy(root);
const pm = JSON.parse(readFileSync(join(rolesDir, "pm.json"), "utf8"));
pm.credentials = pm.credentials.filter((c) => c.service === "gitea");
writeJson(join(rolesDir, "pm.json"), pm);
refuses(() => check(undefined, { rolesDir }), /has "tracker" but pm uses no vikunja credential/);
refuses(() => check((d) => { delete d.roles.pm.tracker; }, { rolesDir }), /exactly the services its role definition needs \(gitea; got gitea, vikunja\)/);
const business = check((d) => { delete d.roles.pm.tracker; delete d.roles.pm.credentials.vikunja; }, { rolesDir });
assert.equal(business.roles.pm.tracker, null);
});
test("credential references match the definition's services", () => {
refuses(() => check((d) => { delete d.roles.cto.credentials.gitea; }), /exactly the services .* \(gitea, vikunja; got vikunja\)/);
refuses(() => check((d) => { d.roles.cto.credentials.github = { env: "X", rotateBy: "2099-01-01" }; }), /exactly the services/);
refuses(() => check((d) => { d.roles.cto.credentials.gitea = { env: "X" }; }), /needs "rotateBy"/);
refuses(() => check((d) => { d.roles.cto.credentials.vikunja.token = "tk_x"; }), /unsupported .* key: "token"/);
refuses(() => check((d) => { d.tracker.sync.credentials = {}; }), /exactly the services .* \(vikunja; got none\)/);
});
test("launch", () => {
assert.equal(check((d) => { delete d.launch; }).launch, null);
refuses(() => check((d) => { d.launch.by = "ghost"; }), /launch\.by names ghost/);
refuses(() => check((d) => { d.launch.by = "cto"; d.launch.instances = ["coder"]; }), /launch\.by names cto, whose role cto doesn't hold role\.launch within-role/);
refuses(() => check((d) => { d.launch.instances = []; }), /must not be empty/);
refuses(() => check((d) => { d.launch.instances = ["coder", "coder"]; }), /duplicate/);
refuses(() => check((d) => { d.launch.instances = ["ghost"]; }), /instances names ghost/);
refuses(() => check((d) => { d.launch.instances = ["pm"]; }), /can't include the launcher itself/);
refuses(() => check((d) => { d.launch.max = {}; }), /at least one model family/);
refuses(() => check((d) => { d.launch.max = { glm: 1 }; }), /unknown model family "glm"/);
refuses(() => check((d) => { d.launch.max = { opus: 5 }; }), /from 0 to 4/);
refuses(() => check((d) => { d.launch.max = { sonnet: -1 }; }), /from 0 to 4/);
refuses(() => check((d) => { d.launch.max = { opus: 1.5 }; }), /from 0 to 4/);
refuses(() => check((d) => { d.launch.cap = 1; }), /unsupported .* launch key/);
assert.deepEqual(check((d) => { d.launch.max = { opus: 0 }; }).launch.max, { opus: 0 });
});
test("loadBusiness: file checks", () => {
const root = tmp();
const dir = join(root, "config");
refuses(() => loadBusiness("acme", { dir, rolesDir: REPO_ROLES }), /business file not found/, 4);
const file = writeJson(join(dir, "businesses", "acme.json"), businessDoc(root));
assert.equal(loadBusiness("acme", { dir, rolesDir: REPO_ROLES }).file, file);
chmodSync(file, 0o620);
refuses(() => loadBusiness("acme", { dir, rolesDir: REPO_ROLES }), /writable by group or other \(mode 620\)/);
chmodSync(file, 0o602);
refuses(() => loadBusiness("acme", { dir, rolesDir: REPO_ROLES }), /writable by group or other/);
chmodSync(file, 0o644);
assert.equal(loadBusiness("acme", { dir, rolesDir: REPO_ROLES }).id, "acme");
writeJson(join(dir, "businesses", "real.json"), businessDoc(root, "linked"));
symlinkSync("real.json", join(dir, "businesses", "linked.json"));
refuses(() => loadBusiness("linked", { dir, rolesDir: REPO_ROLES }), /non-symbolic-link/, 4);
writeFileSync(join(dir, "businesses", "broken.json"), "{ not json");
refuses(() => loadBusiness("broken", { dir, rolesDir: REPO_ROLES }), /not valid JSON/);
// The owner and mode checks run on the opened file before the read, so
// a group-writable file refuses for its mode even when it won't parse.
chmodSync(join(dir, "businesses", "broken.json"), 0o660);
refuses(() => loadBusiness("broken", { dir, rolesDir: REPO_ROLES }), /writable by group or other \(mode 660\)/);
assert.throws(() => loadBusiness("acme", { dir }), /needs rolesDir/);
});
// A directory or a FIFO under the business name refuses without a read.
// Opening a FIFO without O_NONBLOCK would block the whole process, which
// node:test can't time out, so that case runs in a child with a timeout.
test("loadBusiness: not a regular file", () => {
const dir = join(tmp(), "config");
mkdirSync(join(dir, "businesses", "folder.json"), { recursive: true });
refuses(() => loadBusiness("folder", { dir, rolesDir: REPO_ROLES }), /must be a regular, non-symbolic-link file/, 4);
execFileSync("mkfifo", [join(dir, "businesses", "pipe.json")]);
const index = pathToFileURL(join(REPO, "packages", "business", "src", "index.mjs")).href;
const child = spawnSync(process.execPath, ["--input-type=module", "-e", `
import { loadBusiness } from ${JSON.stringify(index)};
try { loadBusiness("pipe", { dir: ${JSON.stringify(dir)}, rolesDir: ${JSON.stringify(REPO_ROLES)} }); }
catch (error) { console.log(error.exitCode, error.message); }
`], { encoding: "utf8", timeout: 5000 });
assert.equal(child.signal, null, "opening the FIFO waited for a writer");
assert.match(child.stdout, /^4 .*must be a regular, non-symbolic-link file/);
});
test("loading writes nothing", () => {
const root = tmp();
const dir = join(root, "config");
const file = writeJson(join(dir, "businesses", "acme.json"), businessDoc(root));
const listing = (d) => readdirSync(d, { recursive: true }).sort().join("\n");
const before = { tree: listing(root), mtime: lstatSync(file).mtimeMs, text: readFileSync(file, "utf8") };
loadBusiness("acme", { dir, rolesDir: REPO_ROLES });
assert.equal(listing(root), before.tree);
assert.equal(lstatSync(file).mtimeMs, before.mtime);
assert.equal(readFileSync(file, "utf8"), before.text);
});
test("names that are Object.prototype properties don't count as declared", () => {
refuses(() => check((d) => { d.arbiters.delivery = "constructor"; }), /arbiters\.delivery names constructor/);
refuses(() => check((d) => { d.launch.by = "constructor"; }), /launch\.by names constructor/);
refuses(() => check((d) => { d.launch.instances = ["constructor"]; }), /instances names constructor/);
refuses(() => check((d) => { d.launch.max = { constructor: 1 }; }), /unknown model family "constructor"/);
refuses(() => check((d) => { d.roles.pm.definition = "constructor"; }), /not found/, 4);
const labels = check((d) => { d.tracker.labels = JSON.parse('{"__proto__": 5}'); }).tracker.labels;
assert.equal(Object.getPrototypeOf(labels), Object.prototype);
assert.equal(Object.getOwnPropertyDescriptor(labels, "__proto__").value, 5);
});
test("the shipped example refuses as written and validates once filled in", () => {
const example = readFileSync(join(REPO, "packages", "business", "examples", "mosaic-stack.example.json"), "utf8");
const root = tmp();
const file = join(root, "businesses", "mosaic-stack.json");
refuses(() => validateBusinessDocument(JSON.parse(example), file, { rolesDir: REPO_ROLES }), /botId must be a positive integer/);
let id = 10;
const filled = JSON.parse(example
.replaceAll('"botId": 0', () => `"botId": ${id++}`)
.replaceAll("YYYY-MM-DD", "2099-01-01")
.replaceAll("/home/you", root));
const business = validateBusinessDocument(filled, file, { rolesDir: REPO_ROLES });
assert.deepEqual(Object.keys(business.roles), ["pm", "cto", "coder", "reviewer"]);
assert.equal(business.tracker.sync.credentials.vikunja.file, join(root, ".config/mosaic-dev/secrets/mosaic-stack/sync-vikunja.token"));
assert.equal(id, 15);
});