Files
stack/packages/organization/tests/lifecycle-settings.test.mjs

266 lines
15 KiB
JavaScript

// Row 194 (#1692): the organization writer's delete (V5), the human's veto lists (V6) and the
// chief's hire and fire (D2.2). The broker's identity, decision and allowlist checks are bus tests.
// 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 { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { instanceSettings } from "../src/instance-settings.mjs";
import { OrganizationError, organizationWriter } from "../src/organization.mjs";
import { CREATIVE_DEFINITIONS } from "../src/vocabulary.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 })));
const T0 = "2026-10-18T07:00:00.000Z";
const CONTEXT = Object.freeze({ instance: "chief", run: "run-1", reason: "a security review is due" });
// sec-1 is a listed security-reviewer: no credential, no tracker bot, so the writer may delete it.
function fixture(mutate) {
const root = tmp();
roots.push(root);
const rolesDir = rolesCopy(root);
const doc = businessDoc(root, "acme");
doc.roles["sec-1"] = { definition: "security-reviewer" };
doc.launch.instances.push("sec-1");
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) });
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));
const onDisk = () => JSON.parse(readFileSync(file, "utf8"));
const revision = () => settings.read("acme").revision;
return { root, dir, rolesDir, file, settings, validate, log, onDisk, revision };
}
// Each refusal: its code (or the validator's BusinessError), and the file's bytes unchanged.
function refuses(f, act, code) {
const bytes = readFileSync(f.file, "utf8");
assert.throws(act, e => (code === "BusinessError" ? e.name === code : e.code === code), code);
assert.equal(readFileSync(f.file, "utf8"), bytes, code);
}
const del = (f, instance, extra = {}) => () => f.settings.set("acme", { revision: f.revision(), instance, op: "delete", ...extra }, "jason");
const veto = (f, op, name, actor = "human") => f.settings.veto("acme", { revision: f.revision(), op, name }, actor);
const fire = (f, name) => f.settings.fire("acme", { name }, { actor: "chief", context: CONTEXT });
test("T6 delete refuses in the design's order and leaves the file's bytes alone", () => {
const f = fixture(doc => {
doc.roles.assistant = { definition: "assistant", holder: "mos" };
doc.roles["sec-3"] = { definition: "security-reviewer" };
doc.launch.keep = ["sec-3", "coder"];
});
refuses(f, del(f, "ghost"), "organization-unknown-instance");
refuses(f, del(f, "assistant"), "organization-definition-refused");
refuses(f, del(f, "chief"), "organization-instance-referenced");
refuses(f, del(f, "pm"), "organization-instance-referenced");
refuses(f, del(f, "cto"), "organization-instance-referenced");
assert.throws(del(f, "cto"), /cto is arbiters\.technical; change that first/);
refuses(f, del(f, "sec-3"), "organization-instance-kept");
// coder declares credentials and is kept: kept is checked first, so the veto is named.
refuses(f, del(f, "coder"), "organization-instance-kept");
refuses(f, del(f, "reviewer"), "organization-credentials-out-of-scope");
refuses(f, del(f, "sec-1", { definition: "security-reviewer" }), "invalid-request");
// checkDelete runs the same checks with no write.
refuses(f, () => f.settings.checkDelete("acme", "sec-3"), "organization-instance-kept");
const bytes = readFileSync(f.file, "utf8");
assert.deepEqual(f.settings.checkDelete("acme", "sec-1"), { revision: f.revision() });
assert.equal(readFileSync(f.file, "utf8"), bytes);
});
test("T6 delete refuses the last listed instance", () => {
const f = fixture(doc => { doc.launch.instances = ["sec-1"]; });
refuses(f, del(f, "sec-1"), "BusinessError");
refuses(f, () => f.settings.checkDelete("acme", "sec-1"), "BusinessError");
});
test("T6 a clean delete removes the entry and its own references, adds retired, and the name can't come back", () => {
const f = fixture(doc => { doc.launch.limits.tokens = { instances: { "sec-1": { limit: 100, since: T0 }, coder: { limit: 50, since: T0 } } }; });
const done = del(f, "sec-1")();
assert.equal(Object.hasOwn(done.instances, "sec-1"), false);
assert.deepEqual(done.retired, ["sec-1"]);
assert.equal(done.restart, false);
const file = f.onDisk();
assert.equal(Object.hasOwn(file.roles, "sec-1"), false);
assert.deepEqual(file.launch.instances, ["coder", "reviewer"]);
assert.deepEqual(file.launch.limits.tokens.instances, { coder: { limit: 50, since: T0 } });
assert.deepEqual(file.retired, ["sec-1"]);
const run = f.validate();
assert.equal(run.status, 0, run.stderr);
const entry = f.log().at(-1);
assert.equal(entry.actor, "jason");
assert.equal(entry.transaction, done.transaction);
assert.ok(entry.changes.some(c => c.path === "roles.sec-1" && c.newPresent === false));
assert.ok(entry.changes.some(c => c.path === "retired" && c.oldPresent === false));
refuses(f, () => f.settings.set("acme", { revision: f.revision(), instance: "sec-1", op: "add", definition: "security-reviewer" }, "jason"),
"organization-instance-retired");
assert.equal(f.settings.hire, undefined, "direct hire writer removed; decisions are covered in hire-decisions.test.mjs");
refuses(f, del(f, "sec-1"), "organization-unknown-instance");
});
test("T7 veto: keep, unkeep, nohire and hire each write once, a repeat is no change, and the log names the human", () => {
const f = fixture();
let view = veto(f, "keep", "sec-1");
assert.equal(view.instances["sec-1"].kept, true);
assert.equal(view.restart, false);
assert.deepEqual(f.onDisk().launch.keep, ["sec-1"]);
let entry = f.log().at(-1);
assert.equal(entry.actor, "human");
assert.ok(entry.changes.some(c => c.path === "launch.keep" && c.oldPresent === false && c.new.includes("sec-1")));
refuses(f, () => veto(f, "keep", "sec-1"), "organization-no-change");
view = veto(f, "unkeep", "sec-1");
assert.equal(view.instances["sec-1"].kept, false);
assert.deepEqual(f.onDisk().launch.keep, []);
refuses(f, () => veto(f, "unkeep", "sec-1"), "organization-no-change");
view = veto(f, "nohire", "merge-gate");
assert.deepEqual(view.noHire, ["merge-gate"]);
// A name with no role file is accepted: the list forbids, it doesn't declare.
view = veto(f, "nohire", "no-such-role");
assert.deepEqual(view.noHire, ["merge-gate", "no-such-role"]);
refuses(f, () => veto(f, "nohire", "merge-gate"), "organization-no-change");
view = veto(f, "hire", "merge-gate");
assert.deepEqual(view.noHire, ["no-such-role"]);
refuses(f, () => veto(f, "hire", "merge-gate"), "organization-no-change");
entry = f.log().at(-1);
assert.equal(entry.actor, "human");
assert.ok(entry.changes.some(c => c.path === "launch.noHire"));
assert.equal(f.validate().status, 0);
});
test("T7 veto refuses an unknown or END_PROTECTED keep, a bad request, a stale revision and a business without launch", () => {
const f = fixture(doc => { doc.roles.assistant = { definition: "assistant", holder: "mos" }; });
refuses(f, () => veto(f, "keep", "ghost"), "organization-unknown-instance");
refuses(f, () => veto(f, "keep", "assistant"), "organization-definition-refused");
const revision = f.revision();
for (const bad of [
{ revision, op: "keep" }, { revision, op: "forbid", name: "sec-1" }, { revision, op: "keep", name: "Bad Name" },
{ revision: "stale", op: "keep", name: "sec-1" }, { revision, op: "keep", name: "sec-1", by: "chief" },
]) refuses(f, () => f.settings.veto("acme", bad, "human"), "invalid-request");
refuses(f, () => f.settings.veto("acme", null, "human"), "invalid-request");
veto(f, "keep", "sec-1");
refuses(f, () => f.settings.veto("acme", { revision, op: "nohire", name: "coder" }, "human"), "revision-conflict");
const none = fixture(doc => { delete doc.launch; doc.roles["sec-1"] = { definition: "security-reviewer" }; });
refuses(none, () => veto(none, "nohire", "coder"), "organization-launch-not-configured");
});
test("D2.2 fire delists and keeps the entry, a repeat is done with no write, and each refusal holds", () => {
const f = fixture(doc => {
doc.roles.assistant = { definition: "assistant", holder: "mos" };
doc.roles["sec-3"] = { definition: "security-reviewer" };
doc.launch.instances.push("sec-3");
doc.launch.keep = ["sec-3"];
});
const done = fire(f, "sec-1");
assert.equal(done.delisted, true);
assert.equal(done.instances["sec-1"].launched, false);
assert.deepEqual(f.onDisk().roles["sec-1"], { definition: "security-reviewer" });
assert.deepEqual(f.onDisk().launch.instances, ["coder", "reviewer", "sec-3"]);
const entry = f.log().at(-1);
assert.equal(entry.actor, "chief");
assert.deepEqual(entry.context, CONTEXT);
const bytes = readFileSync(f.file, "utf8");
const again = fire(f, "sec-1");
assert.equal(again.delisted, false);
assert.equal(again.transaction, null);
assert.equal(readFileSync(f.file, "utf8"), bytes);
refuses(f, () => fire(f, "ghost"), "organization-unknown-instance");
refuses(f, () => fire(f, "Bad Name"), "organization-unknown-instance");
refuses(f, () => fire(f, "sec-3"), "instance-kept");
refuses(f, () => fire(f, "assistant"), "organization-definition-refused");
refuses(f, () => fire(f, "chief"), "organization-instance-referenced");
assert.equal(f.validate().status, 0);
const one = fixture(doc => { doc.launch.instances = ["sec-1"]; });
refuses(one, () => fire(one, "sec-1"), "BusinessError");
});
// Row 256 (#1761): checkDelete takes the actor, and fire is unchanged: a creative instance refuses
// every actor's fire, the human's included.
test("row 256 checkDelete passes the human's creative delete and refuses another actor's; fire still refuses", () => {
const f = fixture(doc => {
doc.roles.bean = { definition: "companion", credentials: { gitea: { store: true, rotateBy: FAR_DATE } } };
doc.launch.instances.push("bean");
});
const bytes = readFileSync(f.file, "utf8");
assert.deepEqual(f.settings.checkDelete("acme", "bean", "jason"), { revision: f.revision() });
for (const actor of ["chief", "tester", undefined]) refuses(f, () => f.settings.checkDelete("acme", "bean", actor), "organization-creative-human-only");
for (const actor of ["chief", "jason"]) refuses(f, () => f.settings.fire("acme", { name: "bean" }, { actor, context: CONTEXT }), "organization-definition-refused");
assert.throws(() => f.settings.checkFire("acme", "bean"), e => e.code === "organization-definition-refused");
assert.equal(readFileSync(f.file, "utf8"), bytes);
});
// Row 239 (#1745): a fire's or a delete's revoke runs inside the writer, under its lock, after the
// revision check. revoking(f, writer, fail) builds settings on f's files with a spy revoke and that writer.
function revoking(f, writer, fail = null) {
const calls = [];
const revoke = (business, agent, how, services) => {
calls.push([business, agent, how, services]);
if (fail) throw fail;
};
const settings = instanceSettings({ dir: f.dir, rolesDir: f.rolesDir, system: systemFor(f.root), revoke, ...(writer ? { writer } : {}) });
return { settings, calls };
}
const receipts = f => (existsSync(f.settings.writer.logFile("acme")) ? f.log() : []);
// A hand edit: the same document in other bytes, so the revision changes and the file still validates.
const handEdit = f => {
const bytes = JSON.stringify(f.onDisk()) + "\n";
writeFileSync(f.file, bytes);
return bytes;
};
test("row 239 T1 a fire on a revision that goes stale before the writer's lock refuses there and revokes nothing", () => {
const f = fixture();
const real = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir });
let edited = null;
// The edit lands after commit()'s early check and before the writer takes its lock.
const writer = { ...real, write: (id, document, options) => { edited = handEdit(f); return real.write(id, document, options); } };
const { settings, calls } = revoking(f, writer);
const before = receipts(f).length;
assert.throws(() => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT }),
e => e.code === "revision-conflict" && e.revoked === undefined);
assert.deepEqual(calls, []);
assert.equal(readFileSync(f.file, "utf8"), edited);
assert.equal(receipts(f).length, before);
});
test("row 239 T2 a write after the revoke refused before publication carries revoked, and the file keeps the edit", () => {
const f = fixture();
let edited = null;
const writer = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: stage => { if (stage === "prepared") edited = handEdit(f); } });
const { settings, calls } = revoking(f, writer);
assert.throws(() => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT }),
e => e.code === "revision-conflict" && e.revoked === true);
assert.deepEqual(calls, [["acme", "sec-1", "fire", []]]);
assert.equal(readFileSync(f.file, "utf8"), edited);
assert.equal(receipts(f).at(-1).state, "aborted");
});
test("row 239 T3 any error after the revoke carries revoked, for a fire and a delete; a revoke that throws carries nothing", () => {
for (const [how, act] of [
["fire", settings => settings.fire("acme", { name: "sec-1" }, { actor: "chief", context: CONTEXT })],
["delete", (settings, f) => settings.set("acme", { revision: f.revision(), instance: "sec-1", op: "delete" }, "jason")],
]) {
const f = fixture();
const bytes = readFileSync(f.file, "utf8");
const writer = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: stage => { if (stage === "prepared") throw new Error("stopped at prepared"); } });
const { settings, calls } = revoking(f, writer);
assert.throws(() => act(settings, f), e => e.message === "stopped at prepared" && e.revoked === true, how);
assert.equal(calls.length, 1, how);
assert.equal(calls[0][2], how);
assert.equal(readFileSync(f.file, "utf8"), bytes, how);
const g = fixture();
const gBytes = readFileSync(g.file, "utf8");
const failing = revoking(g, null, new OrganizationError("credential-revoke-failed"));
assert.throws(() => act(failing.settings, g), e => e.code === "credential-revoke-failed" && e.revoked === undefined, how);
assert.equal(failing.calls.length, 1, how);
assert.equal(readFileSync(g.file, "utf8"), gBytes, how);
assert.deepEqual(receipts(g), [], how);
}
});