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