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]>
195 lines
9.8 KiB
JavaScript
195 lines
9.8 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("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";
|
|
}, {
|
|
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 pm = resolve(setup(), "pm");
|
|
assert.equal(classify(pm, "role.launch"), "within");
|
|
assert.deepEqual(pm.launch.instances, ["coder", "reviewer"]);
|
|
assert.equal(resolve(setup(), "cto").launch, null);
|
|
|
|
const noLaunch = resolve(setup((d) => { delete d.launch; }), "pm");
|
|
assert.equal(classify(noLaunch, "role.launch"), "gated");
|
|
assert.equal(noLaunch.launch, null);
|
|
|
|
// A second pm instance is the launcher, so the first one may not launch.
|
|
const two = setup((d) => {
|
|
d.roles.pm2 = { ...d.roles.pm, holder: undefined, tracker: { bot: "bot-acme-pm2", botId: 7 } };
|
|
d.launch.by = "pm2";
|
|
});
|
|
assert.equal(classify(resolve(two, "pm"), "role.launch"), "gated");
|
|
assert.equal(resolve(two, "pm").launch, null);
|
|
assert.equal(classify(resolve(two, "pm2"), "role.launch"), "within");
|
|
assert.equal(resolve(two, "pm2").launch.by, "pm2");
|
|
});
|
|
|
|
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.pm.vars["limits.authority"] = authority; })
|
|
: setup(undefined, { roles: { pm: { vars: { "limits.authority": authority } } } });
|
|
const pm = resolve(s, "pm");
|
|
assert.equal(classify(pm, "role.launch"), "gated", layer);
|
|
assert.equal(pm.launch, null, layer);
|
|
}
|
|
});
|
|
|
|
test("limits.authority narrows cross-role actions too", () => {
|
|
const s = setup((d) => {
|
|
d.roles.coder.vars["limits.authority"] = ["task.update.assigned", "git.push.working", "task.scope.change"];
|
|
});
|
|
const coder = resolve(s, "coder");
|
|
assert.deepEqual(coder.limits.authority, { withinRole: ["task.update.assigned", "git.push.working"], crossRole: ["task.scope.change"] });
|
|
assert.equal(classify(coder, "task.reassign"), "gated");
|
|
assert.equal(classify(coder, "task.scope.change"), "cross");
|
|
assert.equal(classify(coder, "review.request"), "gated");
|
|
});
|
|
|
|
test("classify", () => {
|
|
const s = setup();
|
|
const coder = resolve(s, "coder");
|
|
assert.equal(classify(coder, "git.push.working"), "within");
|
|
assert.equal(classify(coder, "task.reassign"), "cross");
|
|
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", "tracker", "vars",
|
|
]);
|
|
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/);
|
|
});
|