Files
stack/packages/business/tests/instance-settings.test.mjs
T
jason.woltje c216f47dbb plugins, cli, bus, business, webui: harness, model and thinking from the instance vars, a fallback route, and a harness plug-in kind (row 65, #1558)
Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi
and claude-code hold what core branched on: the adapter, thinking levels,
preflight, version probe, sandbox mounts, builtins line, typed tool names,
gate timeout, bundle files, adapter env, turn end and session-files path.
Core reaches a module only through the loader.

A session's route is the instance's harness, model and thinking vars, or
the three fallback. vars; a missing one refuses no-route, and the system
file's execution keys are never read for a session. The launcher checks
the primary route, then the fallback: harness-off, harness-refused,
thinking-unsupported, then preflight. A failed preflight falls back when
a fallback is set, and any refusal of the fallback names the primary's
failure too. Account checks run
only when the route's module counts an account. The manifest records
route, routeReason and harnessVersion, replacing piVersion and
claudeVersion. pi gets --thinking, Claude Code --effort, both from
MOSAIC_THINKING.

The business file refuses a partial fallback. routes and routes.set are
human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by
routes.set, instances.set and limits.set and checks only changed routes.
The chief of staff changes an instance's model, and the assistant's whole
vars (Q169), through limits.set, so its route write runs the same checks.
A model outside launch.models refuses unknown-model, primary or fallback,
and routes.set passes that code through.
A new run binds only on a switched-on harness module name and records
its transcript path, which the usage reader uses. Settings gains a
Routing table with six selects per instance and loses the Role models
fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is
unchanged.

Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new
launch refuses harness-off.
2026-10-19 11:27:19 -05:00

217 lines
16 KiB
JavaScript

// Row 165 (#1662, Q171, REQ-ROLE-3): the human adds a role instance and changes one's definition,
// vars and launch membership through the organization writer. holder, credentials and tracker
// stay hand edits; a refused write leaves the file's bytes alone.
import test, { after } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { instanceSettings } from "../src/instance-settings.mjs";
import { projectFilePath } from "../src/project.mjs";
import { businessDoc, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
const CLI = join(REPO, "packages", "business", "src", "cli.mjs");
const roots = [];
after(() => roots.forEach(root => rmSync(root, { recursive: true, force: true })));
// The business file sits where mosaic business validate looks for it, so the test can run it.
// Row 65: a write that sets a harness needs it switched on in plugins.harness.
const harnessOn = (doc, names = ["pi", "claude-code"]) => { doc.plugins = { ...(doc.plugins ?? {}), harness: names }; };
const LEVELS = { pi: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], "claude-code": ["low", "medium", "high", "xhigh", "max"] };
function fixture(mutate, { levels } = {}) {
const root = tmp();
roots.push(root);
const rolesDir = rolesCopy(root);
const doc = businessDoc(root, "acme");
mutate?.(doc, root);
const dir = join(root, "config");
const file = writeJson(join(dir, "businesses", "acme.json"), doc);
const config = systemConfig(root);
const settings = instanceSettings({ dir, rolesDir, system: systemFor(root), ...(levels ? { levels } : {}) });
const validate = () => spawnSync(process.execPath, [CLI, "validate", "acme"], {
env: { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: rolesDir }, encoding: "utf8",
});
const log = () => readFileSync(settings.writer.logFile("acme"), "utf8").trim().split("\n").map(line => JSON.parse(line));
return { root, rolesDir, doc, file, settings, validate, log };
}
// Each refusal: its code (or the validator's BusinessError), and the file's bytes unchanged.
function refuses(f, args, code) {
const bytes = readFileSync(f.file, "utf8");
const revision = f.settings.read("acme").revision;
assert.throws(() => f.settings.set("acme", { revision, ...args }, "jason"),
e => (code === "BusinessError" ? e.name === code : e.code === code), JSON.stringify(args));
assert.equal(readFileSync(f.file, "utf8"), bytes, JSON.stringify(args));
}
test("row 165 read lists each instance and the definitions add accepts", () => {
const f = fixture(doc => { doc.roles.assistant = { definition: "assistant", holder: "mos" }; });
const view = f.settings.read("acme");
assert.match(view.revision, /^[a-f0-9]{64}$/);
assert.equal(view.launch, true);
assert.deepEqual(view.instances.coder, { definition: "coder", vars: { harness: "pi", "limits.network": "none" }, launched: true, launcher: false, protected: false });
assert.equal(view.instances.chief.launcher, true);
assert.equal(view.instances.assistant.protected, true);
assert.deepEqual(view.definitions["security-reviewer"], { services: [], accepted: true });
assert.deepEqual(view.definitions.coder, { services: ["gitea", "vikunja"], accepted: false });
assert.equal(view.definitions.assistant.accepted, false);
assert.equal(Object.hasOwn(view.definitions, "researcher"), false);
assert.equal(Object.hasOwn(view.definitions, "conductor-policy"), false);
});
test("row 165 add: two instances join, validate exits 0, the log names the human, and the bus restart is reported", () => {
const f = fixture();
const view = f.settings.read("acme");
const added = f.settings.set("acme", { revision: view.revision, instance: "sec-1", op: "add", definition: "security-reviewer", vars: { model: "claude-sonnet-5-5" }, launch: true }, "jason");
assert.deepEqual(added.instances["sec-1"], { definition: "security-reviewer", vars: { model: "claude-sonnet-5-5" }, launched: true, launcher: false, protected: false });
assert.notEqual(added.revision, view.revision);
assert.match(added.transaction, /\S/);
assert.equal(added.restart, true);
const gate = f.settings.set("acme", { revision: added.revision, instance: "gate-1", op: "add", definition: "merge-gate" }, "jason");
assert.deepEqual(gate.instances["gate-1"], { definition: "merge-gate", vars: {}, launched: false, launcher: false, protected: false });
const file = JSON.parse(readFileSync(f.file, "utf8"));
assert.deepEqual(file.roles["gate-1"], { definition: "merge-gate" });
assert.deepEqual(file.launch.instances, ["coder", "reviewer", "sec-1"]);
const run = f.validate();
assert.equal(run.status, 0, run.stderr);
assert.ok(JSON.parse(run.stdout).instances["sec-1"]);
const [first, second] = f.log().filter(e => e.state === "committed");
assert.equal(first.actor, "jason");
assert.equal(first.transaction, added.transaction);
assert.ok(first.changes.some(c => c.path === "roles.sec-1" && c.oldPresent === false && c.new.definition === "security-reviewer"));
assert.ok(first.changes.some(c => c.path === "launch.instances" && c.new.includes("sec-1") && !c.old.includes("sec-1")));
assert.equal(second.actor, "jason");
});
test("row 165 change: definition, vars and launch membership log before and after; only a definition change asks for a restart", () => {
const f = fixture(doc => { harnessOn(doc); doc.roles["sec-1"] = { definition: "security-reviewer", vars: { harness: "pi" } }; });
const redefined = f.settings.set("acme", { revision: f.settings.read("acme").revision, instance: "sec-1", op: "change", definition: "merge-gate" }, "jason");
assert.equal(redefined.instances["sec-1"].definition, "merge-gate");
assert.equal(redefined.restart, true);
let entry = f.log().at(-1);
assert.ok(entry.changes.some(c => c.path === "roles.sec-1.definition" && c.old === "security-reviewer" && c.new === "merge-gate"));
const revarred = f.settings.set("acme", { revision: redefined.revision, instance: "sec-1", op: "change", vars: { harness: "claude-code", model: "claude-opus-5-5" } }, "jason");
assert.deepEqual(revarred.instances["sec-1"].vars, { harness: "claude-code", model: "claude-opus-5-5" });
assert.equal(revarred.restart, false);
entry = f.log().at(-1);
assert.ok(entry.changes.some(c => c.path === "roles.sec-1.vars.harness" && c.old === "pi" && c.new === "claude-code"));
assert.ok(entry.changes.some(c => c.path === "roles.sec-1.vars.model" && c.oldPresent === false && c.new === "claude-opus-5-5"));
const launched = f.settings.set("acme", { revision: revarred.revision, instance: "sec-1", op: "change", launch: true }, "jason");
assert.equal(launched.instances["sec-1"].launched, true);
assert.equal(launched.restart, false);
const dropped = f.settings.set("acme", { revision: launched.revision, instance: "reviewer", op: "change", launch: false }, "jason");
assert.deepEqual(JSON.parse(readFileSync(f.file, "utf8")).launch.instances, ["coder", "sec-1"]);
assert.deepEqual(f.log().at(-1).changes.find(c => c.path === "launch.instances"),
{ path: "launch.instances", old: ["coder", "reviewer", "sec-1"], new: ["coder", "sec-1"], oldPresent: true, newPresent: true });
assert.equal(f.settings.read("acme").revision, dropped.revision);
assert.equal(f.validate().status, 0);
});
test("row 165 refuses each validation failure and leaves the file's bytes alone", () => {
const f = fixture(doc => { doc.roles["sec-1"] = { definition: "security-reviewer" }; });
refuses(f, { instance: "pm", op: "add", definition: "merge-gate" }, "organization-instance-exists");
refuses(f, { instance: "ghost", op: "change", vars: {} }, "organization-unknown-instance");
refuses(f, { instance: "sec-2", op: "add", definition: "no-such-role" }, "BusinessError");
refuses(f, { instance: "sec-2", op: "add", definition: "researcher" }, "invalid-request");
refuses(f, { instance: "sec-2", op: "add", definition: "merge-gate", vars: { "no.such.var": 1 } }, "BusinessError");
refuses(f, { instance: "sec-2", op: "add", definition: "merge-gate", vars: { model: "not-in-the-table" } }, "BusinessError");
refuses(f, { instance: "chief", op: "change", launch: true }, "BusinessError");
refuses(f, { instance: "chief", op: "change", definition: "merge-gate" }, "BusinessError");
refuses(f, { instance: "coder", op: "change", launch: true }, "organization-no-change");
refuses(f, { instance: "sec-1", op: "change", definition: "security-reviewer" }, "organization-no-change");
for (const bad of [
{ instance: "Bad Name", op: "add", definition: "merge-gate" }, { instance: "sec-2", op: "remove" }, { instance: "sec-2", op: "add" },
{ instance: "sec-1", op: "change" }, { instance: "sec-2", op: "add", definition: "merge-gate", holder: "mos" },
{ instance: "sec-2", op: "add", definition: "merge-gate", credentials: {} }, { instance: "sec-2", op: "add", definition: "merge-gate", tracker: {} },
{ instance: "sec-2", op: "add", definition: "merge-gate", vars: [] }, { instance: "sec-2", op: "add", definition: "merge-gate", launch: "yes" },
]) refuses(f, bad, "invalid-request");
// Dewey F1: the broker refuses a business whose roles name human or tester, so the writer does too.
refuses(f, { instance: "human", op: "add", definition: "merge-gate" }, "invalid-request");
refuses(f, { instance: "tester", op: "add", definition: "merge-gate" }, "invalid-request");
const one = fixture(doc => { doc.launch.instances = ["coder"]; });
refuses(one, { instance: "coder", op: "change", launch: false }, "BusinessError");
const none = fixture(doc => { delete doc.launch; });
refuses(none, { instance: "sec-2", op: "add", definition: "merge-gate", launch: true }, "organization-launch-not-configured");
});
// mos-0823: no instance is written without its credential binding, and nothing binds one here.
test("row 165 refuses an added instance whose definition declares credentials, and says why", () => {
const f = fixture();
refuses(f, { instance: "reviewer-2", op: "add", definition: "reviewer" }, "organization-credentials-out-of-scope");
assert.throws(() => f.settings.set("acme", { revision: f.settings.read("acme").revision, instance: "reviewer-2", op: "add", definition: "reviewer" }, "jason"),
/reviewer declares gitea and vikunja credentials/);
});
test("row 165 refuses a definition change to or from one that declares credentials", () => {
const f = fixture(doc => { doc.roles["sec-1"] = { definition: "security-reviewer" }; });
refuses(f, { instance: "sec-1", op: "change", definition: "coder" }, "organization-credentials-out-of-scope");
refuses(f, { instance: "reviewer", op: "change", definition: "security-reviewer" }, "organization-credentials-out-of-scope");
refuses(f, { instance: "reviewer", op: "change", definition: "cto" }, "organization-credentials-out-of-scope");
});
test("row 165 a vars or launch change on an instance with its binding succeeds and keeps the binding", () => {
const f = fixture(doc => harnessOn(doc));
const before = JSON.parse(readFileSync(f.file, "utf8")).roles.coder;
const done = f.settings.set("acme", { revision: f.settings.read("acme").revision, instance: "coder", op: "change", vars: { harness: "claude-code" }, launch: false }, "jason");
assert.equal(done.restart, false);
const after = JSON.parse(readFileSync(f.file, "utf8")).roles.coder;
assert.deepEqual(after, { ...before, vars: { harness: "claude-code" } });
assert.deepEqual(after.credentials, before.credentials);
assert.deepEqual(after.tracker, before.tracker);
assert.equal(f.log().at(-1).changes.some(c => c.path.startsWith("roles.coder.credentials") || c.path.startsWith("roles.coder.tracker")), false);
assert.equal(f.validate().status, 0);
});
test("row 165 refuses an END_PROTECTED definition and every change to a protected instance", () => {
const f = fixture(doc => { doc.roles.assistant = { definition: "assistant", holder: "mos" }; doc.roles["sec-1"] = { definition: "security-reviewer" }; });
refuses(f, { instance: "assistant-2", op: "add", definition: "assistant" }, "organization-definition-refused");
refuses(f, { instance: "sec-1", op: "change", definition: "assistant" }, "organization-definition-refused");
refuses(f, { instance: "assistant", op: "change", definition: "merge-gate" }, "organization-definition-refused");
refuses(f, { instance: "assistant", op: "change", vars: { model: "claude-opus-5-5" } }, "organization-definition-refused");
refuses(f, { instance: "assistant", op: "change", launch: true }, "organization-definition-refused");
});
test("row 165 refuses while a project file doesn't resolve, as validate would", () => {
const ghost = fixture((doc, root) => writeJson(projectFilePath(join(root, "project")), { projectVersion: 1, id: "stack", roles: { ghost: { vars: {} } } }));
refuses(ghost, { instance: "sec-2", op: "add", definition: "merge-gate" }, "organization-unresolvable-instance");
const misnamed = fixture((doc, root) => writeJson(projectFilePath(join(root, "project")), { projectVersion: 1, id: "other" }));
refuses(misnamed, { instance: "sec-2", op: "add", definition: "merge-gate" }, "organization-project-mismatch");
const good = fixture((doc, root) => writeJson(projectFilePath(join(root, "project")), { projectVersion: 1, id: "stack", roles: { coder: { vars: {} } } }));
const done = good.settings.set("acme", { revision: good.settings.read("acme").revision, instance: "sec-2", op: "add", definition: "merge-gate" }, "jason");
assert.equal(done.instances["sec-2"].definition, "merge-gate");
});
test("row 165 refuses a stale revision and leaves the file's bytes alone", () => {
const f = fixture();
const stale = f.settings.read("acme").revision;
f.settings.set("acme", { revision: stale, instance: "sec-1", op: "add", definition: "security-reviewer" }, "jason");
const bytes = readFileSync(f.file, "utf8");
assert.throws(() => f.settings.set("acme", { revision: stale, instance: "gate-1", op: "add", definition: "merge-gate" }, "jason"), e => e.code === "revision-conflict");
assert.equal(readFileSync(f.file, "utf8"), bytes);
});
// Row 65 (REQ-PLUG-2): a write that sets a harness that isn't switched on, a thinking level its
// module doesn't apply, a model that isn't a launch.models key or a partial fallback refuses, and
// the file keeps its bytes. A route the write doesn't touch isn't rechecked for its harness.
test("row 65 an instance write refuses harness-off, thinking-unsupported, an unknown model and a partial fallback", () => {
const off = fixture();
refuses(off, { instance: "coder", op: "change", vars: { harness: "claude-code" } }, "harness-off");
// The file's own unswitched harnesses don't block a write that leaves them alone.
const kept = off.settings.set("acme", { revision: off.settings.read("acme").revision, instance: "coder", op: "change", vars: { harness: "pi", "limits.network": "none", "limits.tools": [] } }, "jason");
assert.equal(kept.restart, false);
const f = fixture(doc => harnessOn(doc, ["pi"]), { levels: name => LEVELS[name] ?? null });
refuses(f, { instance: "coder", op: "change", vars: { harness: "claude-code" } }, "harness-off");
refuses(f, { instance: "coder", op: "change", vars: { harness: "pi", "fallback.harness": "claude-code", "fallback.model": "claude-opus-5-5", "fallback.thinking": "high" } }, "harness-off");
const g = fixture(doc => harnessOn(doc), { levels: name => LEVELS[name] ?? null });
refuses(g, { instance: "coder", op: "change", vars: { harness: "claude-code", thinking: "minimal" } }, "thinking-unsupported");
// The business file's own launch check names an unknown model first.
refuses(g, { instance: "coder", op: "change", vars: { harness: "pi", model: "ghost" } }, "BusinessError");
refuses(g, { instance: "coder", op: "change", vars: { harness: "pi", "fallback.harness": "pi" } }, "BusinessError");
const done = g.settings.set("acme", { revision: g.settings.read("acme").revision, instance: "coder", op: "change", vars: { harness: "claude-code", model: "claude-opus-5-5", thinking: "max", "fallback.harness": "pi", "fallback.model": "claude-sonnet-5-5", "fallback.thinking": "minimal" } }, "jason");
assert.deepEqual(done.instances.coder.vars, { harness: "claude-code", model: "claude-opus-5-5", thinking: "max", "fallback.harness": "pi", "fallback.model": "claude-sonnet-5-5", "fallback.thinking": "minimal" });
// A module that didn't load gives no levels, so a thinking write on it refuses.
const h = fixture(doc => harnessOn(doc, ["pi", "fixture"]), { levels: name => LEVELS[name] ?? null });
refuses(h, { instance: "coder", op: "change", vars: { harness: "fixture", thinking: "high" } }, "harness-refused");
});