Review "changes 8" fix round for org-redact6. - launcher: a harness module's throw names its system code or class, never its message; preflight, loader, bundle, checkout, sandbox and route text is relayed with host paths replaced (relay()). - claude-code and pi preflights name MOSAIC_CLAUDE_BIN and the repository's node_modules, never a path; persona and skill bundle errors and sandboxProblem name their file or the sandbox by label. - seat session: thread store, legacy store, overlap, busy lock and newRun refusals name the place under the data root. - discord: unknown keys, binding names and turn ids are described when not plain; approvals, journal and repository read errors name the file and code; git drops stderr lines that carry a host path and a spawn failure names its code only. - seat, control-board: registration fields and seen keys are described when not plain; invalid seat name and layout state the constraint. - organization: the registry names the config directory and registry file by label; --project is quoted only when it is a plain name; systemConfigProblem moves to its own module. - scripts/mosaic-config.mjs: the config by where it came from, the data root and its marker by role, keys and arguments only when plain, and error codes in place of raw messages. Exit codes unchanged. - webui sso-config: an unknown console key is named only when plain. Sentinel tests in each package's path-redaction-labels file fail on BASE; pins that quoted a path now assert the label and its absence.
384 lines
28 KiB
JavaScript
384 lines
28 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 { createHash } from "node:crypto";
|
|
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { instanceSettings } from "../src/instance-settings.mjs";
|
|
import { projectFilePath } from "../src/project.mjs";
|
|
import { businessDoc, FAR_DATE, REPO, rolesCopy, systemConfig, systemFor, tmp, writeJson } from "./helpers.mjs";
|
|
|
|
const CLI = join(REPO, "packages", "organization", "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"] };
|
|
|
|
test('C2-04: instance settings skip a library while checking a later code project', async () => {
|
|
const { mappingEntry } = await import('../../library/tests/helpers.mjs');
|
|
const f = fixture(doc => { doc.roles.chief.credentials = { gitea: { ...doc.roles.coder.credentials.gitea } }; doc.projects = { books: mappingEntry(), ...doc.projects }; });
|
|
writeJson(projectFilePath(f.doc.projects.stack.root), { projectVersion: 1, id: 'stack', vars: { 'tracker.project': 3 } });
|
|
const before = f.settings.read('acme');
|
|
f.settings.set('acme', { revision: before.revision, instance: 'gate-215', op: 'add', definition: 'merge-gate' }, 'jason');
|
|
assert.deepEqual(JSON.parse(readFileSync(f.file, 'utf8')).projects.books, mappingEntry());
|
|
assert.ok(f.settings.read('acme').instances['gate-215']);
|
|
});
|
|
|
|
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, kept: false, endProtected: 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 writer asks for no restart (row 194, D3)", () => {
|
|
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, kept: false, endProtected: false });
|
|
assert.notEqual(added.revision, view.revision);
|
|
assert.match(added.transaction, /\S/);
|
|
assert.equal(added.restart, false);
|
|
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, kept: false, endProtected: 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; none asks for a restart, the bus reloads (row 194, D3)", () => {
|
|
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, false);
|
|
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");
|
|
// FED R3 (dewey-0777): and user, the granted user's principal.
|
|
refuses(f, { instance: "user", op: "add", definition: "merge-gate" }, "invalid-request");
|
|
// A4 decision-path name refusals are covered by hire-decisions.test.mjs.
|
|
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.
|
|
// Row 194 (brief item 7, sage-1605, sage-1610 a): the human's add of a credentialed definition
|
|
// writes store references dated today and no file. A vikunja definition takes an existing bot,
|
|
// checked on the add path before the file's own validation: its shape, and no other instance or
|
|
// the sync bot using its name or id. A definition without vikunja takes none.
|
|
test("T19 add of a credentialed definition writes store references dated today, names its bot, and validate exits 0", () => {
|
|
const f = fixture();
|
|
const add = (instance, definition, extra = {}) => ({ instance, op: "add", definition, ...extra });
|
|
refuses(f, add("reviewer-2", "reviewer"), "organization-tracker-required");
|
|
refuses(f, add("reviewer-2", "reviewer", { tracker: { bot: "reviewer-bot", botId: 40 } }), "invalid-request");
|
|
refuses(f, add("reviewer-2", "reviewer", { tracker: { bot: "bot-acme-reviewer-2", botId: 0 } }), "invalid-request");
|
|
refuses(f, add("reviewer-2", "reviewer", { tracker: { bot: "bot-acme-reviewer", botId: 40 } }), "organization-tracker-bot-taken");
|
|
refuses(f, add("reviewer-2", "reviewer", { tracker: { bot: "bot-acme-reviewer-2", botId: f.doc.roles.pm.tracker.botId } }), "organization-tracker-bot-taken");
|
|
refuses(f, add("reviewer-2", "reviewer", { tracker: { bot: "bot-acme-sync", botId: 40 } }), "organization-tracker-bot-taken");
|
|
refuses(f, add("sec-1", "security-reviewer", { tracker: { bot: "bot-acme-sec-1", botId: 40 } }), "invalid-request");
|
|
refuses(f, { instance: "coder", op: "change", vars: {}, tracker: { bot: "bot-acme-coder-2", botId: 40 } }, "invalid-request");
|
|
|
|
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const done = f.settings.set("acme", { revision: f.settings.read("acme").revision, ...add("reviewer-2", "reviewer", { tracker: { bot: "bot-acme-reviewer-2", botId: 40 } }) }, "jason");
|
|
assert.equal(done.restart, false);
|
|
assert.deepEqual(JSON.parse(readFileSync(f.file, "utf8")).roles["reviewer-2"], {
|
|
definition: "reviewer",
|
|
tracker: { bot: "bot-acme-reviewer-2", botId: 40 },
|
|
credentials: { gitea: { store: true, rotateBy: today }, vikunja: { store: true, expires: today } },
|
|
});
|
|
const validated = f.validate();
|
|
assert.equal(validated.status, 0, validated.stderr);
|
|
assert.match(validated.stderr, /roles\.reviewer-2\.credentials\.vikunja store file: not issued/);
|
|
// The Console's create form keeps listing it as not accepted.
|
|
assert.equal(f.settings.read("acme").definitions.reviewer.accepted, false);
|
|
});
|
|
|
|
// webby-0094 N2: a definition that also declares the host service gets store references for its
|
|
// token services only. host is a grant, not a reference, and an "undefined" date key refused the add.
|
|
test("T19 add of a definition that declares host writes references for its token services only", () => {
|
|
const f = fixture();
|
|
const file = join(f.rolesDir, "reviewer.json");
|
|
const role = JSON.parse(readFileSync(file, "utf8"));
|
|
role.credentials.push({ service: "host" });
|
|
writeFileSync(file, JSON.stringify(role));
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
f.settings.set("acme", { revision: f.settings.read("acme").revision, instance: "reviewer-2", op: "add", definition: "reviewer", tracker: { bot: "bot-acme-reviewer-2", botId: 40 } }, "jason");
|
|
assert.deepEqual(JSON.parse(readFileSync(f.file, "utf8")).roles["reviewer-2"].credentials,
|
|
{ gitea: { store: true, rotateBy: today }, vikunja: { store: true, expires: today } });
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
// Row 213 (C2-08, #1714, M10): the human adds and changes a creative instance here, its persona
|
|
// vars included; the chief of staff and the tester are refused. The persona text never enters the
|
|
// business file, a read or the change log: the vars hold only its name and digest.
|
|
test("row 213 a creative instance is the human's: add, vars and definition changes, persona digest only", () => {
|
|
const f = fixture(doc => { doc.roles["sec-1"] = { definition: "security-reviewer" }; });
|
|
const marker = "PERSONA-MARKER-7f3a";
|
|
const text = `Warm and brief. ${marker}\n`;
|
|
mkdirSync(join(f.root, "config", "personas", "acme"), { recursive: true, mode: 0o700 });
|
|
writeFileSync(join(f.root, "config", "personas", "acme", "bean.md"), text, { mode: 0o600 });
|
|
const persona = { "persona.name": "Bean", "persona.sha256": createHash("sha256").update(text).digest("hex") };
|
|
const view = f.settings.read("acme");
|
|
// Row 285 (mos-1332 item 4): the companion definition declares gitea.
|
|
assert.deepEqual(view.definitions.companion, { services: ["gitea"], accepted: true });
|
|
const add = { instance: "bean", op: "add", definition: "companion", vars: persona };
|
|
for (const actor of ["chief-of-staff.chief", "tester"]) {
|
|
assert.throws(() => f.settings.set("acme", { revision: view.revision, ...add }, actor), e => e.code === "organization-creative-human-only", actor);
|
|
assert.throws(() => f.settings.set("acme", { revision: view.revision, instance: "sec-1", op: "change", definition: "ideas" }, actor),
|
|
e => e.code === "organization-creative-human-only", `${actor} redefine`);
|
|
}
|
|
let done = f.settings.set("acme", { revision: view.revision, ...add }, "jason");
|
|
// Row 256: the listing a write returns is read for the writer, and the human may delete bean.
|
|
assert.deepEqual(done.instances.bean, { definition: "companion", vars: persona, launched: false, launcher: false, protected: false, kept: false, endProtected: false });
|
|
for (const actor of ["chief-of-staff.chief", "tester"]) {
|
|
for (const change of [{ vars: {} }, { launch: true }, { definition: "security-reviewer" }]) {
|
|
assert.throws(() => f.settings.set("acme", { revision: done.revision, instance: "bean", op: "change", ...change }, actor),
|
|
e => e.code === "organization-creative-human-only", `${actor} ${JSON.stringify(change)}`);
|
|
}
|
|
}
|
|
done = f.settings.set("acme", { revision: done.revision, instance: "bean", op: "change", definition: "ideas", vars: { ...persona, "persona.name": "Bean Two" } }, "jason");
|
|
assert.equal(done.instances.bean.definition, "ideas");
|
|
// Row 194 (D3): the writer's restart is false; the bus sets it from its reload, and bean isn't running.
|
|
assert.equal(done.restart, false);
|
|
// A persona var on a definition that isn't creative is refused by the validator (D1).
|
|
refuses(f, { instance: "bean", op: "change", definition: "security-reviewer" }, "BusinessError");
|
|
assert.equal(f.validate().status, 0);
|
|
assert.deepEqual(f.log().filter(e => e.state === "committed").map(e => e.actor), ["jason", "jason"]);
|
|
for (const out of [JSON.stringify(f.settings.read("acme")), readFileSync(f.settings.writer.logFile("acme"), "utf8"), readFileSync(f.file, "utf8")]) {
|
|
assert.ok(!out.includes(marker));
|
|
}
|
|
// The writer's modes hold for the new file and its history.
|
|
assert.equal(statSync(f.file).mode & 0o777, 0o600);
|
|
// changes.jsonl sits in the history dir; the loop below covers it too, but the brief names it.
|
|
assert.equal(statSync(f.settings.writer.logFile("acme")).mode & 0o777, 0o600);
|
|
const history = join(f.root, "config", "businesses", ".history", "acme");
|
|
assert.ok(readdirSync(history).includes("changes.jsonl"));
|
|
assert.equal(statSync(history).mode & 0o777, 0o700);
|
|
for (const name of readdirSync(history)) {
|
|
const st = statSync(join(history, name));
|
|
assert.equal(st.mode & 0o777, st.isDirectory() ? 0o700 : 0o600, name);
|
|
}
|
|
});
|
|
|
|
// Row 256 (#1761, REQ-COS-3): the human deletes a creative instance and any other actor is refused
|
|
// with the creative code. The assistant, the END_PROTECTED definition that isn't creative, refuses
|
|
// every actor, the human included. The listing's endProtected is that same check for its reader.
|
|
test("row 256 T1-T3, T5 the human deletes a creative instance, no other actor does, and the assistant stays", () => {
|
|
const f = fixture(doc => {
|
|
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
|
doc.roles.muse = { definition: "ideas", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
|
doc.roles.assistant = { definition: "assistant", holder: "mos" };
|
|
});
|
|
const del = (instance, actor) => () => f.settings.set("acme", { revision: f.settings.read("acme").revision, instance, op: "delete" }, actor);
|
|
const refused = (act, code, label) => {
|
|
const bytes = readFileSync(f.file, "utf8");
|
|
assert.throws(act, e => e.code === code, label);
|
|
assert.equal(readFileSync(f.file, "utf8"), bytes, label);
|
|
};
|
|
// T5: the reader's view. A read with no actor answers as for any non-human.
|
|
for (const [actor, bean] of [["jason", false], ["tester", true], ["chief-of-staff.chief", true], [undefined, true]]) {
|
|
const view = f.settings.read("acme", actor);
|
|
assert.equal(view.instances.bean.endProtected, bean, `bean ${actor}`);
|
|
assert.equal(view.instances.assistant.endProtected, true, `assistant ${actor}`);
|
|
assert.equal(view.instances.coder.endProtected, false, `coder ${actor}`);
|
|
}
|
|
assert.equal(f.settings.read("acme").instances.bean.endProtected, true);
|
|
// T2: every actor but the human gets the creative code, and the message says why.
|
|
for (const actor of ["chief-of-staff.chief", "tester", "mos", undefined]) {
|
|
refused(del("bean", actor), "organization-creative-human-only", String(actor));
|
|
assert.throws(() => f.settings.checkDelete("acme", "bean", actor), e => e.code === "organization-creative-human-only", `check ${actor}`);
|
|
}
|
|
assert.throws(del("bean", "tester"), /bean is a companion instance, which only the human deletes/);
|
|
// T3: the assistant refuses every actor, the human included.
|
|
for (const actor of ["jason", "chief-of-staff.chief", "tester"]) {
|
|
refused(del("assistant", actor), "organization-definition-refused", `assistant ${actor}`);
|
|
}
|
|
// T1: the human's delete.
|
|
assert.deepEqual(f.settings.checkDelete("acme", "bean", "jason"), { revision: f.settings.read("acme").revision });
|
|
const done = del("bean", "jason")();
|
|
assert.equal(Object.hasOwn(done.instances, "bean"), false);
|
|
assert.deepEqual(done.retired, ["bean"]);
|
|
assert.equal(done.instances.muse.endProtected, false);
|
|
const file = JSON.parse(readFileSync(f.file, "utf8"));
|
|
assert.equal(Object.hasOwn(file.roles, "bean"), false);
|
|
assert.deepEqual(file.retired, ["bean"]);
|
|
assert.equal(f.log().at(-1).actor, "jason");
|
|
const run = f.validate();
|
|
assert.equal(run.status, 0, run.stderr);
|
|
});
|
|
|
|
// Row 256 (R1, webby-0122): a creative delete by the human goes on to the later checks. A creative
|
|
// instance can't be kept, launch.by or an arbiter, so the check it reaches is the last-listed one.
|
|
test("row 256 T4 the human's delete of the only listed instance, a creative one, is the last-listed refusal", () => {
|
|
const f = fixture(doc => {
|
|
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
|
|
doc.launch.instances = ["bean"];
|
|
});
|
|
const bytes = readFileSync(f.file, "utf8");
|
|
const revision = f.settings.read("acme").revision;
|
|
assert.throws(() => f.settings.set("acme", { revision, instance: "bean", op: "delete" }, "jason"), e => e.name === "BusinessError");
|
|
assert.throws(() => f.settings.checkDelete("acme", "bean", "jason"), e => e.name === "BusinessError");
|
|
assert.throws(() => f.settings.set("acme", { revision, instance: "bean", op: "delete" }, "tester"), e => e.code === "organization-creative-human-only");
|
|
assert.equal(readFileSync(f.file, "utf8"), bytes);
|
|
});
|