// 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, /reviewer-2\/vikunja: 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); });