Files
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

211 lines
11 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { join } from "node:path";
import {
BusinessError, classify, projectFilePath, resolveInstance, systemVars, validateBusinessDocument, validateProjectDocument,
} from "../src/index.mjs";
import { businessDoc, REPO_ROLES, systemFor, tmp } from "./helpers.mjs";
function refuses(fn, pattern) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.equal(error.exitCode, 2);
assert.match(error.message, pattern);
return true;
});
}
function setup(mutate, projectDoc) {
const root = tmp();
const doc = businessDoc(root);
mutate?.(doc);
const business = validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir: REPO_ROLES });
const project = projectDoc
? validateProjectDocument({ projectVersion: 1, id: "stack", ...projectDoc }, projectFilePath(doc.projects.stack.root))
: null;
return { root, business, project, system: systemFor(root) };
}
const resolve = (s, instance) => resolveInstance({ system: s.system, business: s.business, project: s.project, instance });
test('C2-04: a library first in projects does not hide a later project layer', async t => {
const { mappingEntry } = await import('../../library/tests/helpers.mjs');
const { rmSync } = await import('node:fs');
const s = setup(doc => { doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; doc.projects = { books: mappingEntry(), ...doc.projects }; }, { vars: { 'tracker.project': 7 } });
t.after(() => rmSync(s.root, { recursive: true, force: true }));
const result = resolve(s, 'coder');
assert.equal(result.project, 'stack');
assert.equal(result.vars['tracker.project'], 7);
});
test("systemVars flattens the validated config", () => {
const vars = systemVars({ environment: "development", dataRoot: "/d", execution: { backend: "docker", provider: "zai", model: "m", adapter: "pi" } });
assert.deepEqual({ ...vars }, {
environment: "development", dataRoot: "/d", "execution.backend": "docker",
"execution.provider": "zai", "execution.model": "m", "execution.adapter": "pi",
});
refuses(() => systemVars({ environment: "development", dataRoot: "relative", execution: { backend: "b", provider: "p", model: "m", adapter: "a" } }), /dataRoot must be an absolute path/);
});
test("precedence: system, business, project, project role, agent", () => {
const s = setup((d) => {
d.roles.coder.vars.model = "agent-model";d.launch.models["agent-model"]={provider:"zai",model:"agent-model"};
}, {
vars: { "tracker.pollSeconds": 20, "tracker.project": 3 },
roles: { coder: { vars: { "tracker.pollSeconds": 15 } } },
});
const coder = resolve(s, "coder");
assert.equal(coder.project, "stack");
assert.equal(coder.vars["tracker.pollSeconds"], 15);
assert.equal(coder.provenance["tracker.pollSeconds"], "project:stack:roles.coder");
assert.equal(coder.vars["tracker.project"], 3);
assert.equal(coder.provenance["tracker.project"], "project:stack");
assert.equal(coder.vars["tracker.baseUrl"], "http://127.0.0.1:3456");
assert.equal(coder.provenance["tracker.baseUrl"], "business:acme");
assert.equal(coder.vars.model, "agent-model");
assert.equal(coder.provenance.model, "business:acme:roles.coder");
assert.equal(coder.vars["execution.adapter"], "mock");
assert.equal(coder.provenance["execution.adapter"], "system");
assert.equal(coder.provenance["tracker.reconcileMinutes"], "default");
const pm = resolve(s, "pm");
assert.equal(pm.vars["tracker.pollSeconds"], 20);
assert.equal(pm.provenance["tracker.pollSeconds"], "project:stack");
const noProject = resolveInstance({ system: s.system, business: s.business, instance: "pm" });
assert.equal(noProject.project, null);
assert.equal(noProject.vars["tracker.pollSeconds"], 60);
});
test("limits narrow the definition and never widen it", () => {
const s = setup((d) => {
d.vars["limits.network"] = "open";
d.roles.cto.vars["limits.tools"] = ["read", "grep", "bash", "write"];
d.roles.reviewer.vars = { "limits.tools": ["read", "write"], "limits.authority": ["review.verdict", "task.close"] };
}, { roles: { cto: { vars: { "limits.tools": ["read", "bash", "write", "edit"] } } } });
const coder = resolve(s, "coder");
assert.equal(coder.limits.network, "none");
assert.deepEqual(coder.provenance["limits.network"], ["business:acme", "business:acme:roles.coder"]);
assert.equal(resolve(s, "pm").limits.network, "api-only");
const cto = resolve(s, "cto");
assert.deepEqual(cto.limits.tools, ["read", "write", "bash"]);
assert.deepEqual(cto.provenance["limits.tools"], ["project:stack:roles.cto", "business:acme:roles.cto"]);
const reviewer = resolve(s, "reviewer");
assert.deepEqual(reviewer.limits.tools, ["read"]);
assert.deepEqual(reviewer.limits.authority, { withinRole: ["review.verdict"], crossRole: [] });
assert.equal(classify(reviewer, "message.send"), "gated");
assert.equal(classify(reviewer, "task.close"), "gated");
});
test("role.launch stays within-role only for the instance the launch block names", () => {
const chief = resolve(setup(), "chief");
assert.equal(classify(chief, "role.launch"), "within");
assert.deepEqual(chief.launch.instances, ["coder", "reviewer"]);
assert.equal(resolve(setup(), "cto").launch, null);
// The pm no longer launches (decision 139): its role file lacks the action.
const pm = resolve(setup(), "pm");
assert.equal(classify(pm, "role.launch"), "gated");
assert.equal(pm.launch, null);
const noLaunch = resolve(setup((d) => { delete d.launch; }), "chief");
assert.equal(classify(noLaunch, "role.launch"), "gated");
assert.equal(noLaunch.launch, null);
// A second chief-of-staff instance is the launcher, so the first one may not launch.
const two = setup((d) => {
d.roles.chief2 = { definition: "chief-of-staff" };
d.launch.by = "chief2";
});
assert.equal(classify(resolve(two, "chief"), "role.launch"), "gated");
assert.equal(resolve(two, "chief").launch, null);
assert.equal(classify(resolve(two, "chief2"), "role.launch"), "within");
assert.equal(resolve(two, "chief2").launch.by, "chief2");
});
test("limits.authority without role.launch leaves the launcher with no launch block", () => {
for (const layer of ["agent", "project"]) {
const authority = ["task.create", "task.assign", "message.send"];
const s = layer === "agent"
? setup((d) => { d.roles.chief.vars = { "limits.authority": authority }; })
: setup(undefined, { roles: { chief: { vars: { "limits.authority": authority } } } });
const chief = resolve(s, "chief");
assert.equal(classify(chief, "role.launch"), "gated", layer);
assert.equal(chief.launch, null, layer);
}
});
test("limits.authority narrows cross-role actions too", () => {
// pm, the role with two cross-role actions: the limit keeps one and drops the other.
const s = setup((d) => {
d.roles.pm.vars["limits.authority"] = ["task.create", "task.close", "task.priority.change"];
});
const pm = resolve(s, "pm");
assert.deepEqual(pm.limits.authority, { withinRole: ["task.create", "task.close"], crossRole: ["task.priority.change"] });
assert.equal(classify(pm, "task.priority.change"), "cross");
assert.equal(classify(pm, "task.scope.change"), "gated");
assert.equal(classify(pm, "task.assign"), "gated");
});
test("classify", () => {
const s = setup();
const coder = resolve(s, "coder");
assert.equal(classify(coder, "git.push.working"), "within");
assert.equal(classify(coder, "task.scope.change"), "cross");
assert.equal(classify(coder, "task.reassign"), "gated");
assert.equal(classify(coder, "task.close"), "gated");
assert.equal(classify(coder, "deploy"), "gated");
assert.equal(classify(resolve(s, "pm"), "task.priority.change"), "cross");
assert.equal(classify(resolve(s, "cto"), "decision.resolve.technical"), "within");
refuses(() => classify(coder, "task.delete"), /unknown action/);
});
test("the record carries what the broker and launcher need", () => {
const s = setup();
const pm = resolve(s, "pm");
assert.deepEqual(Object.keys(pm).sort(), [
"business", "contract", "credentials", "definition", "digest", "holder", "instance", "launch", "limits", "project", "provenance", "skills", "tracker", "vars", "webSource",
]);
assert.equal(pm.contract, join(REPO_ROLES, "pm.md"));
assert.equal(pm.holder, "sage");
assert.deepEqual(pm.tracker, { bot: "bot-acme-pm", botId: 3 });
assert.deepEqual(Object.keys(pm.credentials).sort(), ["gitea", "vikunja"]);
assert.match(pm.credentials.gitea.file, /pm-gitea\.token$/);
assert.ok(Object.isFrozen(pm.limits.authority.withinRole));
});
test("digest: key order doesn't matter, any value change does", () => {
const root = tmp();
const file = join(root, "businesses", "acme.json");
const load = (mutate) => {
const doc = businessDoc(root);
mutate?.(doc);
return validateBusinessDocument(doc, file, { rolesDir: REPO_ROLES });
};
const system = systemFor(root);
const digest = (business, instance = "coder", sys = system) => resolveInstance({ system: sys, business, instance }).digest;
const base = digest(load());
assert.match(base, /^[0-9a-f]{64}$/);
assert.equal(digest(load()), base);
assert.equal(digest(load((d) => { d.roles.coder.vars = { "limits.network": "none", harness: "pi" }; })), base);
assert.equal(digest(load((d) => { d.vars = Object.fromEntries(Object.entries(d.vars).reverse()); })), base);
assert.notEqual(digest(load((d) => { d.roles.coder.vars.harness = "claude-code"; })), base);
assert.notEqual(digest(load((d) => { d.roles.coder.holder = "rocko"; })), base);
assert.notEqual(digest(load((d) => { d.roles.coder.credentials.gitea.rotateBy = "2098-01-01"; })), base);
assert.notEqual(digest(load(), "coder", { ...system, "execution.model": "other" }), base);
assert.notEqual(digest(load(), "reviewer"), base);
});
test("refusals", () => {
const s = setup();
refuses(() => resolveInstance({ system: s.system, business: s.business, instance: "ghost" }), /declares no role instance "ghost"/);
refuses(() => resolveInstance({ system: s.system, business: s.business, instance: "constructor" }), /declares no role instance/);
const misnamed = setup(undefined, {});
const wrongId = validateProjectDocument({ projectVersion: 1, id: "other" }, misnamed.project.file);
refuses(() => resolveInstance({ system: misnamed.system, business: misnamed.business, project: wrongId, instance: "pm" }), /isn't declared under that id/);
const elsewhere = validateProjectDocument({ projectVersion: 1, id: "stack" }, "/elsewhere/.mosaic/project.json");
refuses(() => resolveInstance({ system: s.system, business: s.business, project: elsewhere, instance: "pm" }), /isn't declared under that id/);
const extraRole = setup(undefined, { roles: { ghost: { vars: {} } } });
refuses(() => resolve(extraRole, "pm"), /sets vars for role instance ghost/);
});