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]>
115 lines
6.4 KiB
JavaScript
115 lines
6.4 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { BusinessError, LAYERS, REGISTRY, checkVars, mergeVars } from "../src/index.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;
|
|
});
|
|
}
|
|
|
|
test("every key names known layers and a merge rule", () => {
|
|
for (const [key, entry] of Object.entries(REGISTRY)) {
|
|
assert.ok(entry.layers.length > 0, key);
|
|
for (const layer of entry.layers) assert.ok(LAYERS.includes(layer), `${key}: ${layer}`);
|
|
assert.ok(["replace", "intersect"].includes(entry.merge), key);
|
|
if (entry.merge === "intersect") assert.ok(key.startsWith("limits."), key);
|
|
}
|
|
});
|
|
|
|
test("unknown keys and wrong layers refuse", () => {
|
|
refuses(() => checkVars({ colour: "blue" }, "business", "b.vars"), /b\.vars: unknown variable "colour"/);
|
|
refuses(() => checkVars({ harness: "pi" }, "business", "b.vars"), /can't be set at the business layer \(allowed: agent\)/);
|
|
refuses(() => checkVars({ "tracker.baseUrl": "http://x" }, "project", "p.vars"), /project layer/);
|
|
refuses(() => checkVars({ "tracker.project": 3 }, "agent", "a.vars"), /agent layer/);
|
|
refuses(() => checkVars({ dataRoot: "/x" }, "business", "b.vars"), /business layer/);
|
|
refuses(() => checkVars(["harness"], "agent", "a.vars"), /JSON object/);
|
|
refuses(() => checkVars({ constructor: 1 }, "agent", "a.vars"), /unknown variable "constructor"/);
|
|
assert.throws(() => checkVars({}, "fleet", "x"), /unknown layer/);
|
|
});
|
|
|
|
test("types", () => {
|
|
const ok = (vars, layer) => assert.deepEqual({ ...checkVars(vars, layer, "v") }, vars);
|
|
ok({ "tracker.baseUrl": "https://tasks.example:3456", "tracker.reconcileMinutes": 1, "tracker.pollSeconds": 10, "human.discordUserId": "123456789012345678" }, "business");
|
|
ok({ "tracker.project": 3, "git.workingBranch": "refactor", "git.protectedBranches": ["main", "next"], suites: ["scripts/test-task.sh"], "issues.repo": "mosaicstack/stack" }, "project");
|
|
ok({ harness: "claude-code", model: "claude-opus-5-5", thinking: "high", "limits.tools": [], "limits.network": "none", "limits.authority": ["message.send"] }, "agent");
|
|
|
|
const bad = [
|
|
[{ "tracker.baseUrl": "ftp://x" }, "business", /http or https/],
|
|
[{ "tracker.baseUrl": "https://x/" }, "business", /trailing slash/],
|
|
[{ "tracker.baseUrl": "https://u:p@x" }, "business", /no credentials/],
|
|
[{ "tracker.baseUrl": "https://x?a=1" }, "business", /query/],
|
|
[{ "tracker.baseUrl": "not a url" }, "business", /http or https URL/],
|
|
[{ "tracker.kind": "jira" }, "business", /one of: vikunja/],
|
|
[{ "tracker.pollSeconds": 9 }, "business", /at least 10/],
|
|
[{ "tracker.pollSeconds": "30" }, "project", /at least 10/],
|
|
[{ "tracker.reconcileMinutes": 0 }, "business", /at least 1/],
|
|
[{ "human.discordUserId": 123456789012345678 }, "business", /as a string/],
|
|
[{ "human.discordUserId": "0123456789012345678" }, "business", /Discord user id/],
|
|
[{ "tracker.project": 0 }, "project", /positive integer/],
|
|
[{ "git.workingBranch": "-x" }, "project", /git branch name/],
|
|
[{ "git.workingBranch": "a..b" }, "project", /git branch name/],
|
|
[{ "git.workingBranch": "x.lock" }, "project", /git branch name/],
|
|
[{ "git.workingBranch": "feat/" }, "project", /git branch name/],
|
|
[{ "git.protectedBranches": [] }, "project", /must not be empty/],
|
|
[{ "git.protectedBranches": ["main", "main"] }, "project", /duplicate/],
|
|
[{ suites: "scripts/test-task.sh" }, "project", /must be an array/],
|
|
[{ "issues.repo": "stack" }, "project", /owner\/name/],
|
|
[{ harness: "codex" }, "agent", /one of: pi, claude-code/],
|
|
[{ thinking: "max" }, "agent", /one of/],
|
|
[{ model: "" }, "agent", /non-empty string/],
|
|
[{ "limits.tools": ["render3d"] }, "agent", /unsupported tool/],
|
|
[{ "limits.network": "lan" }, "agent", /one of: none, api-only, open/],
|
|
[{ "limits.authority": ["deploy.prod"] }, "agent", /unknown action/],
|
|
];
|
|
for (const [vars, layer, pattern] of bad) refuses(() => checkVars(vars, layer, "v"), pattern);
|
|
});
|
|
|
|
test("merge: defaults, then the most specific layer wins", () => {
|
|
const { vars, provenance } = mergeVars([
|
|
{ layer: "business", source: "business", vars: checkVars({ "tracker.pollSeconds": 60 }, "business", "b") },
|
|
{ layer: "project", source: "project", vars: checkVars({ "tracker.pollSeconds": 20 }, "project", "p") },
|
|
]);
|
|
assert.equal(vars["tracker.pollSeconds"], 20);
|
|
assert.equal(provenance["tracker.pollSeconds"], "project");
|
|
assert.equal(vars["tracker.reconcileMinutes"], 60);
|
|
assert.equal(provenance["tracker.reconcileMinutes"], "default");
|
|
assert.equal(vars["tracker.kind"], "vikunja");
|
|
assert.equal(vars.harness, undefined);
|
|
assert.equal(provenance.harness, undefined);
|
|
});
|
|
|
|
test("merge: limits only narrow, and provenance lists each source", () => {
|
|
const layer = (source, layerName, v) => ({ layer: layerName, source, vars: checkVars(v, layerName, source) });
|
|
const { vars, provenance } = mergeVars([
|
|
layer("business", "business", { "limits.network": "none", "limits.tools": ["read", "grep", "bash"] }),
|
|
layer("project", "project", { "limits.network": "open", "limits.tools": ["read", "bash", "write"] }),
|
|
layer("agent", "agent", { "limits.tools": ["bash", "read", "edit"] }),
|
|
]);
|
|
assert.equal(vars["limits.network"], "none");
|
|
assert.deepEqual(provenance["limits.network"], ["business", "project"]);
|
|
assert.deepEqual(vars["limits.tools"], ["read", "bash"]);
|
|
assert.deepEqual(provenance["limits.tools"], ["business", "project", "agent"]);
|
|
|
|
const widen = mergeVars([
|
|
layer("business", "business", { "limits.authority": ["message.send"] }),
|
|
layer("agent", "agent", { "limits.authority": ["message.send", "task.close"] }),
|
|
]);
|
|
assert.deepEqual(widen.vars["limits.authority"], ["message.send"]);
|
|
const empty = mergeVars([layer("agent", "agent", { "limits.tools": [] })]);
|
|
assert.deepEqual(empty.vars["limits.tools"], []);
|
|
});
|
|
|
|
test("merge doesn't change its inputs", () => {
|
|
const business = checkVars({ "limits.tools": ["read", "bash"] }, "business", "b");
|
|
const snapshot = JSON.stringify(business);
|
|
mergeVars([
|
|
{ layer: "business", source: "business", vars: business },
|
|
{ layer: "agent", source: "agent", vars: checkVars({ "limits.tools": ["read"] }, "agent", "a") },
|
|
]);
|
|
assert.equal(JSON.stringify(business), snapshot);
|
|
});
|