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", "bus.tester": true }, "business"); ok({ "tracker.project": 3, "git.workingBranch": "refactor", "git.protectedBranches": ["main", "next"], suites: ["scripts/test-task.sh"], "issues.repo": "mosaicstack/stack", "tracker.descendants": true }, "project"); ok({ harness: "claude-code", model: "claude-opus-5-5", thinking: "high", "limits.tools": [], "limits.network": "none", "limits.authority": ["message.send"] }, "agent"); // Row 65: any module name passes here; whether it is switched on is a launch and write check. ok({ harness: "fixture-harness", model: "m", thinking: "off", "fallback.harness": "pi", "fallback.model": "m", "fallback.thinking": "low" }, "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/], [{ "tracker.descendants": "true" }, "project", /true or false/], [{ "tracker.descendants": 1 }, "project", /true or false/], [{ "tracker.descendants": true }, "business", /business layer \(allowed: project\)/], [{ "bus.tester": "true" }, "business", /true or false/], [{ "bus.tester": 1 }, "business", /true or false/], [{ "bus.tester": true }, "project", /project layer \(allowed: business\)/], [{ "bus.tester": true }, "agent", /agent layer \(allowed: business\)/], [{ "bus.tester": true }, "system", /system layer \(allowed: business\)/], [{ "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", /harness module name/], [{ harness: "../pi" }, "agent", /harness module name/], [{ "fallback.harness": "pi" }, "agent", /a fallback sets all of fallback\.harness, fallback\.model, fallback\.thinking or none \(missing fallback\.model, fallback\.thinking\)/], [{ "fallback.harness": "pi", "fallback.model": "m" }, "agent", /missing fallback\.thinking/], [{ "fallback.harness": "pi", "fallback.model": "m", "fallback.thinking": "huge" }, "agent", /one of/], [{ thinking: "huge" }, "agent", /one of/], [{ model: "" }, "agent", /non-empty string/], [{ "limits.tools": ["render3d"] }, "agent", /unsupported tool/], [{ "web.searxng": "https://search.example.org/" }, "business", /base URL/], [{ "web.searxng": "https://search.example.org" }, "agent", /web\.searxng/], [{ "web.searxng": "http://search.example.org" }, "business", /https, or http on loopback/], [{ "web.searxng": "http://10.0.0.5:8888" }, "business", /https, or http on loopback/], [{ "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["tracker.descendants"], false); assert.equal(provenance["tracker.descendants"], "default"); assert.equal(vars["bus.tester"], false); assert.equal(provenance["bus.tester"], "default"); 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); }); test("web.searxng takes https anywhere and http on loopback, so Jason's 127.0.0.1:8888 passes", () => { for (const v of ["https://search.example.org", "http://127.0.0.1:8888", "http://localhost:8888", "http://[::1]:8888"]) { assert.deepEqual(checkVars({ "web.searxng": v }, "business", "v"), { "web.searxng": v }, v); } });