266 lines
15 KiB
JavaScript
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);
|
|
}
|
|
});
|