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); });