A board, workflow or pack write without assertClean now refuses definition-secret-check-required, and the scan covers every key and string in the plan before the writer reads any of it.
426 lines
31 KiB
JavaScript
426 lines
31 KiB
JavaScript
// Starter packs SP3 (design dd878cefc sections 3 and 8, fix round 3 c0a60ed3a): the organization file's packs, board and
|
|
// workflows keys, each project's board and workflows, and the writer's applyPack operation.
|
|
// Fixtures live in a temp directory; nothing reads the real config.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync, rmSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { organizationWriter } from "../src/organization.mjs";
|
|
import { validateBusinessDocument } from "../src/organization-config.mjs";
|
|
import { businessDoc, rolesCopy, tmp, writeJson } from "./helpers.mjs";
|
|
import { canonicalJson, sha256 } from "../src/util.mjs";
|
|
|
|
const SECRET = "sk-test-NOT-A-SECRET-1234";
|
|
const assertClean = (value) => { if (String(value).includes(SECRET)) throw new Error("credential-leak"); };
|
|
const HASH = "a".repeat(64);
|
|
const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" };
|
|
// An inline proposal: a pack definition the apply did not write, kept whole beside its pin.
|
|
const WRITER = { ...PLAIN, instructions: "Write the docs.\n" };
|
|
const WRITER_HASH = sha256(canonicalJson(WRITER));
|
|
const BOARD = {
|
|
columns: [
|
|
{ state: "todo", label: "Backlog" }, { state: "in-progress", label: "Building" },
|
|
{ state: "in-review", label: "Review" }, { state: "blocked", label: "Blocked" }, { state: "done", label: "Merged" },
|
|
],
|
|
layout: "kanban",
|
|
};
|
|
const LIST_BOARD = { ...structuredClone(BOARD), layout: "list" };
|
|
const WORKFLOWS = {
|
|
change: {
|
|
title: "Change", description: "One change from task to merge.",
|
|
steps: [
|
|
{ title: "Write the task", state: "todo", roles: ["pm"] },
|
|
{ title: "Implement", state: "in-progress", roles: ["coder"] },
|
|
{ title: "Approve the merge", state: "done", roles: ["human"] },
|
|
],
|
|
},
|
|
};
|
|
const PACK = {
|
|
id: "coding", version: "1.0.0", sha256: HASH, origin: "shipped", scope: "org", project: null,
|
|
at: "2026-11-12T20:00:00.000Z", by: "jason", added: ["definitions.scout", "board", "workflows.change"],
|
|
proposedHires: [
|
|
{ name: "coder-1", reason: "writes the code", pin: { template: "coder", sha256: HASH } },
|
|
{ name: "scout-1", pin: { definition: "scout", rev: 1 } },
|
|
{ name: "writer-1", pin: { inline: "writer", sha256: WRITER_HASH }, entry: WRITER },
|
|
],
|
|
};
|
|
|
|
function fixture(t, edit = (doc) => doc, barrier) {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const dir = join(root, "config");
|
|
const rolesDir = rolesCopy(root);
|
|
const document = edit(businessDoc(root));
|
|
const file = writeJson(join(dir, "businesses/acme.json"), document);
|
|
return { root, dir, file, rolesDir, document, writer: organizationWriter({ dir, rolesDir, barrier }) };
|
|
}
|
|
const rev = (f) => f.writer.read("acme").revision;
|
|
const doc = (f) => JSON.parse(readFileSync(f.file, "utf8"));
|
|
const bytes = (f) => readFileSync(f.file, "utf8");
|
|
const load = (f, document) => validateBusinessDocument(document, f.file, { rolesDir: f.rolesDir });
|
|
const without = (object, ...keys) => Object.fromEntries(Object.entries(object).filter(([k]) => !keys.includes(k)));
|
|
// An undefined value in `extra` drops that key from the plan.
|
|
const packPlan = (extra = {}) => Object.fromEntries(Object.entries({
|
|
definitions: { scout: PLAIN }, board: BOARD, workflows: WORKFLOWS,
|
|
packs: without({ ...PACK, proposedHires: PACK.proposedHires.slice(0, 2) }, "at", "by", "added"), ...extra,
|
|
}).filter(([, value]) => value !== undefined));
|
|
|
|
test("SP3 red before: a file with a board key loads, and the loaded organization keeps it", (t) => {
|
|
const f = fixture(t);
|
|
const loaded = load(f, { ...f.document, board: BOARD });
|
|
assert.deepEqual(loaded.board, BOARD);
|
|
});
|
|
|
|
test("SP3 valid packs, board and workflows load, and the organization and each project keep them", (t) => {
|
|
const f = fixture(t);
|
|
const document = {
|
|
...f.document, board: BOARD, workflows: WORKFLOWS, packs: [PACK, { ...PACK, origin: "saved", scope: "project", project: "stack", added: ["projects.stack.board", "projects.stack.workflows.change"], proposedHires: [] }],
|
|
projects: { stack: { ...f.document.projects.stack, board: LIST_BOARD, workflows: WORKFLOWS } },
|
|
};
|
|
const loaded = load(f, document);
|
|
assert.deepEqual(loaded.board, BOARD);
|
|
assert.deepEqual(loaded.workflows, WORKFLOWS);
|
|
assert.deepEqual(loaded.packs, document.packs);
|
|
assert.deepEqual(loaded.projects.stack.board, LIST_BOARD);
|
|
assert.deepEqual(loaded.projects.stack.workflows, WORKFLOWS);
|
|
assert.ok(Object.isFrozen(loaded.projects.stack.board));
|
|
// Title and description are optional; the absent keys stay absent.
|
|
const bare = load(f, { ...f.document, workflows: { quick: { steps: [{ title: "Do it", state: "done", roles: ["human"] }] } } });
|
|
assert.deepEqual(bare.workflows, { quick: { steps: [{ title: "Do it", state: "done", roles: ["human"] }] } });
|
|
// A file without the keys loads as it did, with none of them on the loaded object.
|
|
const plain = load(f, f.document);
|
|
for (const key of ["board", "workflows", "packs"]) assert.ok(!Object.hasOwn(plain, key), key);
|
|
assert.ok(!Object.hasOwn(plain.projects.stack, "board"));
|
|
});
|
|
|
|
test("SP3 a workflow naming a role that does not exist still loads (the load checks shape only)", (t) => {
|
|
const f = fixture(t);
|
|
const ghost = { change: { steps: [{ title: "Haunt", state: "todo", roles: ["ghost-role", "no-such-template"] }] } };
|
|
assert.deepEqual(load(f, { ...f.document, workflows: ghost }).workflows, ghost);
|
|
assert.deepEqual(load(f, { ...f.document, projects: { stack: { ...f.document.projects.stack, workflows: ghost } } }).projects.stack.workflows, ghost);
|
|
});
|
|
|
|
test("SP3 a definition a workflow names can still be removed, and the file then still loads", (t) => {
|
|
// Design c0a60ed3a SP3 acceptance: "an org file whose step names a definition removed since loads,
|
|
// and removing that definition succeeds".
|
|
const f = fixture(t);
|
|
const scouting = { scouting: { steps: [{ title: "Scout", state: "todo", roles: ["scout", "human"] }] } };
|
|
const packs = { ...packPlan().packs, proposedHires: [] };
|
|
f.writer.applyPack("acme", { definitions: { scout: PLAIN }, workflows: scouting, packs }, { revision: rev(f), actor: "jason", assertClean });
|
|
f.writer.applyPack("acme", { "projects.stack.workflows": scouting }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).definitions.scout, { ...PLAIN, rev: 1 });
|
|
f.writer.setDefinition("acme", "scout", () => null, { revision: rev(f), actor: "jason", assertClean });
|
|
const after = doc(f);
|
|
assert.equal(after.definitions?.scout, undefined);
|
|
assert.deepEqual(after.workflows, scouting);
|
|
assert.deepEqual(after.projects.stack.workflows, scouting);
|
|
const loaded = load(f, after);
|
|
assert.deepEqual(loaded.workflows, scouting);
|
|
assert.deepEqual(loaded.projects.stack.workflows, scouting);
|
|
// The writer reads it back too; only a new write of that workflow resolves its roles again.
|
|
assert.equal(f.writer.read("acme").revision, rev(f));
|
|
assert.throws(() => f.writer.applyPack("acme", { workflows: { again: scouting.scouting } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
});
|
|
|
|
test("SP3 each invalid shape refuses", (t) => {
|
|
const f = fixture(t);
|
|
const col = (i, patch) => ({ ...BOARD, columns: BOARD.columns.map((c, j) => (j === i ? { ...c, ...patch } : c)) });
|
|
const step = (patch) => ({ change: { ...WORKFLOWS.change, steps: [{ ...WORKFLOWS.change.steps[0], ...patch }] } });
|
|
const pack = (patch) => [{ ...PACK, ...patch }];
|
|
const steps = (n) => Array.from({ length: n }, () => ({ title: "Step", state: "todo", roles: ["human"] }));
|
|
const cases = {
|
|
"board not an object": { board: [] },
|
|
"board unknown key": { board: { ...BOARD, colour: "red" } },
|
|
"board missing layout": { board: without(BOARD, "layout") },
|
|
"board four columns": { board: { ...BOARD, columns: BOARD.columns.slice(0, 4) } },
|
|
"board six columns": { board: { ...BOARD, columns: [...BOARD.columns, BOARD.columns[0]] } },
|
|
"board duplicate state": { board: col(1, { state: "todo" }) },
|
|
"board unknown state": { board: col(0, { state: "backlog" }) },
|
|
"board column unknown key": { board: col(0, { colour: "red" }) },
|
|
"board label empty": { board: col(0, { label: "" }) },
|
|
"board label 33 characters": { board: col(0, { label: "x".repeat(33) }) },
|
|
"board layout grid": { board: { ...BOARD, layout: "grid" } },
|
|
"workflows not an object": { workflows: [] },
|
|
"workflow name not NAME": { workflows: { Change_1: WORKFLOWS.change } },
|
|
"workflow unknown key": { workflows: { change: { ...WORKFLOWS.change, owner: "pm" } } },
|
|
"workflow no steps": { workflows: { change: { ...WORKFLOWS.change, steps: [] } } },
|
|
"workflow 21 steps": { workflows: { change: { steps: steps(21) } } },
|
|
"workflow title 121 characters": { workflows: { change: { ...WORKFLOWS.change, title: "x".repeat(121) } } },
|
|
"workflow description 1001 characters": { workflows: { change: { ...WORKFLOWS.change, description: "x".repeat(1001) } } },
|
|
"step unknown key": { workflows: step({ id: "plan" }) },
|
|
"step missing roles": { workflows: { change: { steps: [{ title: "Step", state: "todo" }] } } },
|
|
"step title 121 characters": { workflows: step({ title: "x".repeat(121) }) },
|
|
"step unknown state": { workflows: step({ state: "doing" }) },
|
|
"step roles empty": { workflows: step({ roles: [] }) },
|
|
"step nine roles": { workflows: step({ roles: ["a", "b", "c", "d", "e", "f", "g", "h", "i"] }) },
|
|
"step role not NAME": { workflows: step({ roles: ["Coder_1"] }) },
|
|
"step duplicate role": { workflows: step({ roles: ["pm", "pm"] }) },
|
|
"packs not a list": { packs: {} },
|
|
"pack unknown key": { packs: pack({ hires: [] }) },
|
|
"pack missing key": { packs: [without(PACK, "sha256")] },
|
|
"pack origin unknown": { packs: pack({ origin: "borrowed" }) },
|
|
"pack scope root": { packs: pack({ scope: "root" }) },
|
|
"org pack with project": { packs: pack({ project: "stack" }) },
|
|
"project pack without project": { packs: pack({ scope: "project", project: null }) },
|
|
"pack id not NAME": { packs: pack({ id: "Coding" }) },
|
|
"pack version not semver": { packs: pack({ version: "v1" }) },
|
|
"pack sha256 not hex": { packs: pack({ sha256: "A".repeat(64) }) },
|
|
"pack at not a timestamp": { packs: pack({ at: "yesterday" }) },
|
|
"pack by not an id": { packs: pack({ by: "Not An Id" }) },
|
|
"pack added bad path": { packs: pack({ added: ["plugins.theme"] }) },
|
|
"pack nine proposed hires": { packs: pack({ proposedHires: Array.from({ length: 9 }, (_, i) => ({ name: `coder-${i}`, pin: { template: "coder", sha256: HASH } })) }) },
|
|
"proposed hire two pins": { packs: pack({ proposedHires: [{ name: "coder-1", pin: { template: "coder", sha256: HASH, inline: "writer" } }] }) },
|
|
"proposed hire no pin": { packs: pack({ proposedHires: [{ name: "coder-1" }] }) },
|
|
"proposed hire unknown key": { packs: pack({ proposedHires: [{ name: "coder-1", level: "org", pin: { template: "coder", sha256: HASH } }] }) },
|
|
"proposed hire duplicate name": { packs: pack({ proposedHires: [{ name: "c", pin: { template: "coder", sha256: HASH } }, { name: "c", pin: { template: "coder", sha256: HASH } }] }) },
|
|
"inline pin old hash-only shape": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: HASH }, entry: WRITER }] }) },
|
|
"inline pin without entry": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH } }] }) },
|
|
"inline pin name not an id": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "Writer One", sha256: WRITER_HASH }, entry: WRITER }] }) },
|
|
"inline pin sha256 not hex": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: "x" }, entry: WRITER }] }) },
|
|
"template pin with entry": { packs: pack({ proposedHires: [{ name: "w", pin: { template: "coder", sha256: HASH }, entry: WRITER }] }) },
|
|
"inline entry not an object": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: "Write the docs." }] }) },
|
|
"inline entry unknown key": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, level: "org" } }] }) },
|
|
"inline entry unknown tool": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, role: { ...WRITER.role, tools: ["teleport"] } } }] }) },
|
|
"inline entry without role": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: without(WRITER, "role") }] }) },
|
|
"inline entry rev zero": { packs: pack({ proposedHires: [{ name: "w", pin: { inline: "writer", sha256: WRITER_HASH }, entry: { ...WRITER, rev: 0 } }] }) },
|
|
"proposed hire rev zero": { packs: pack({ proposedHires: [{ name: "c", pin: { definition: "scout", rev: 0 } }] }) },
|
|
};
|
|
for (const [name, patch] of Object.entries(cases)) {
|
|
assert.throws(() => load(f, { ...f.document, ...patch }), { name: "BusinessError" }, name);
|
|
}
|
|
// The project keys refuse the same shapes, and other project keys still refuse.
|
|
const project = (patch) => ({ ...f.document, projects: { stack: { ...f.document.projects.stack, ...patch } } });
|
|
for (const [name, patch] of Object.entries({
|
|
"project board layout": { board: { ...BOARD, layout: "grid" } },
|
|
"project workflow 0 steps": { workflows: { change: { steps: [] } } },
|
|
"project unknown key": { packs: [] },
|
|
})) assert.throws(() => load(f, project(patch)), { name: "BusinessError" }, name);
|
|
});
|
|
|
|
test("SP3 applyPack writes definitions, board, workflows and the packs entry in one commit", (t) => {
|
|
const f = fixture(t);
|
|
const before = doc(f);
|
|
const out = f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(out.added, ["definitions.scout", "board", "workflows.change"]);
|
|
assert.equal(out.revision, rev(f));
|
|
const after = doc(f);
|
|
assert.deepEqual(after.definitions.scout, { ...PLAIN, rev: 1 });
|
|
assert.deepEqual(after.board, BOARD);
|
|
assert.deepEqual(after.workflows, WORKFLOWS);
|
|
assert.equal(after.packs.length, 1);
|
|
const entry = after.packs[0];
|
|
assert.deepEqual(without(entry, "at"), { ...without(PACK, "at"), by: "jason", added: out.added, proposedHires: PACK.proposedHires.slice(0, 2) });
|
|
assert.match(entry.at, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
// Nothing else changed.
|
|
assert.deepEqual(without(after, "definitions", "board", "workflows", "packs"), without(before, "definitions", "board", "workflows", "packs"));
|
|
// One prepared and one committed receipt, and the receipt logs paths, not values.
|
|
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l));
|
|
assert.deepEqual(log.map((e) => e.state), ["prepared", "committed"]);
|
|
assert.ok(!JSON.stringify(log).includes("Backlog"));
|
|
// A second pack appends; the first entry stays.
|
|
const second = { packs: { ...packPlan().packs, id: "review", proposedHires: [] }, workflows: { review: WORKFLOWS.change } };
|
|
f.writer.applyPack("acme", second, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).packs.map((p) => p.id), ["coding", "review"]);
|
|
assert.deepEqual(doc(f).packs[0], entry);
|
|
assert.deepEqual(doc(f).packs[1].added, ["workflows.review"]);
|
|
});
|
|
|
|
test("SP3 applyPack writes everything or nothing", (t) => {
|
|
// A plan item after the definitions is bad: the valid definition is not written either.
|
|
const f = fixture(t);
|
|
const original = bytes(f);
|
|
const bad = packPlan({ workflows: { change: { steps: [{ title: "Haunt", state: "todo", roles: ["ghost-role"] }] } } });
|
|
assert.throws(() => f.writer.applyPack("acme", bad, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
assert.equal(bytes(f), original);
|
|
const badBoard = packPlan({ board: { ...BOARD, layout: "grid" } });
|
|
assert.throws(() => f.writer.applyPack("acme", badBoard, { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
|
|
assert.equal(bytes(f), original);
|
|
// A fault after the candidate is prepared, with every item checked, leaves the file unchanged.
|
|
const crash = organizationWriter({ dir: f.dir, rolesDir: f.rolesDir, barrier: (stage) => { if (stage === "prepared") throw new Error("fixture crash"); } });
|
|
assert.throws(() => crash.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean }), /fixture crash/);
|
|
assert.equal(bytes(f), original);
|
|
f.writer.recover("acme");
|
|
assert.equal(bytes(f), original);
|
|
assert.equal(JSON.parse(readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").at(-1)).state, "aborted");
|
|
// The same plan then lands whole.
|
|
f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor: "jason", assertClean });
|
|
assert.ok(doc(f).definitions.scout && doc(f).board && doc(f).workflows && doc(f).packs);
|
|
});
|
|
|
|
test("SP3 an agent actor with a definition in the plan refuses human-required; board and workflow writes pass", (t) => {
|
|
const f = fixture(t);
|
|
const original = bytes(f);
|
|
for (const actor of ["coder", "chief-of-staff.chief", "tester"]) {
|
|
assert.throws(() => f.writer.applyPack("acme", packPlan(), { revision: rev(f), actor, assertClean }), { code: "human-required" }, actor);
|
|
}
|
|
// A saved template carries invented roles; an agent never applies one.
|
|
const saved = { packs: { ...packPlan().packs, origin: "saved", proposedHires: [] }, board: BOARD };
|
|
assert.throws(() => f.writer.applyPack("acme", saved, { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "human-required" });
|
|
assert.equal(bytes(f), original);
|
|
// The non-definition half of a pack, and a board write, need no human (Jason's answer 2).
|
|
const half = { packs: { ...packPlan().packs, proposedHires: [{ name: "coder-1", pin: { template: "coder", sha256: HASH } }] }, workflows: WORKFLOWS };
|
|
f.writer.applyPack("acme", half, { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
|
|
assert.equal(doc(f).packs[0].by, "chief-of-staff.chief");
|
|
f.writer.applyPack("acme", { board: BOARD }, { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
|
|
assert.deepEqual(doc(f).board, BOARD);
|
|
});
|
|
|
|
test("SP3 a plan holding any other key refuses", (t) => {
|
|
const f = fixture(t);
|
|
const original = bytes(f);
|
|
for (const [name, plan] of Object.entries({
|
|
plugins: { plugins: { theme: ["dark"] } },
|
|
roles: { roles: { pm: { definition: "pm" } } },
|
|
limits: { limits: { total: 3 } },
|
|
credentials: { credentials: { gitea: { file: "/x" } } },
|
|
launch: { launch: {} },
|
|
copyBindings: { copyBindings: {} },
|
|
autonomy: { autonomy: "near" },
|
|
"projects whole": { projects: { stack: { root: "/x" } } },
|
|
"projects root": { "projects.stack.root": "/x" },
|
|
"projects agents": { "projects.stack.agents": {} },
|
|
"board beside plugins": { board: BOARD, plugins: {} },
|
|
"dotted workflow item": { "workflows.change": WORKFLOWS.change },
|
|
"empty plan": {},
|
|
"not an object": [],
|
|
})) {
|
|
assert.throws(() => f.writer.applyPack("acme", plan, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" }, name);
|
|
}
|
|
assert.equal(bytes(f), original);
|
|
});
|
|
|
|
test("SP3 a board-only plan changes only board, and a project board only that project's board", (t) => {
|
|
const f = fixture(t);
|
|
const before = doc(f);
|
|
f.writer.applyPack("acme", { board: BOARD }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(without(doc(f), "board"), before);
|
|
assert.deepEqual(doc(f).board, BOARD);
|
|
// board.set replaces a board, and null removes it.
|
|
f.writer.applyPack("acme", { board: LIST_BOARD }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).board, LIST_BOARD);
|
|
f.writer.applyPack("acme", { board: null }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f), before);
|
|
// A project's board.
|
|
f.writer.applyPack("acme", { "projects.stack.board": BOARD }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).projects.stack, { ...before.projects.stack, board: BOARD });
|
|
assert.deepEqual(without(doc(f), "projects"), without(before, "projects"));
|
|
assert.deepEqual(f.writer.read("acme").document.projects.stack.board, BOARD);
|
|
// Workflows merge by name; null removes one; a project's workflows likewise.
|
|
f.writer.applyPack("acme", { workflows: WORKFLOWS }, { revision: rev(f), actor: "jason", assertClean });
|
|
f.writer.applyPack("acme", { workflows: { review: WORKFLOWS.change } }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(Object.keys(doc(f).workflows), ["change", "review"]);
|
|
f.writer.applyPack("acme", { workflows: { change: null } }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(Object.keys(doc(f).workflows), ["review"]);
|
|
f.writer.applyPack("acme", { workflows: { review: null } }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.ok(!Object.hasOwn(doc(f), "workflows"));
|
|
f.writer.applyPack("acme", { "projects.stack.workflows": WORKFLOWS }, { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).projects.stack.workflows, WORKFLOWS);
|
|
// Removing what isn't there refuses.
|
|
assert.throws(() => f.writer.applyPack("acme", { board: null }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
assert.throws(() => f.writer.applyPack("acme", { workflows: { nope: null } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
});
|
|
|
|
test("SP3 a workflow in the plan must name an org definition, a V2 template or human", (t) => {
|
|
const f = fixture(t);
|
|
const original = bytes(f);
|
|
const flow = (roles) => ({ workflows: { change: { steps: [{ title: "Step", state: "todo", roles }] } } });
|
|
for (const roles of [["ghost-role"], ["pm", "ghost-role"], ["conductor-policy"]]) {
|
|
assert.throws(() => f.writer.applyPack("acme", flow(roles), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" }, roles.join());
|
|
assert.throws(() => f.writer.applyPack("acme", { "projects.stack.workflows": flow(roles).workflows }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
}
|
|
assert.equal(bytes(f), original);
|
|
// A template, human, and a definition the same plan adds.
|
|
f.writer.applyPack("acme", flow(["pm", "human"]), { revision: rev(f), actor: "jason", assertClean });
|
|
f.writer.applyPack("acme", packPlan({ board: undefined, workflows: { scouting: { steps: [{ title: "Scout", state: "todo", roles: ["scout"] }] } } }), { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(Object.keys(doc(f).workflows), ["change", "scouting"]);
|
|
});
|
|
|
|
test("SP3 applyPack keeps setDefinition's definition rules", (t) => {
|
|
const f = fixture(t, (d) => ({ ...d, definitions: { scout: { ...PLAIN, rev: 1 }, spare: { ...PLAIN, rev: 2 } }, roles: { ...d.roles, looker: { definition: "org:scout" } } }));
|
|
const original = bytes(f);
|
|
const plan = (definitions) => packPlan({ definitions, board: undefined, workflows: undefined });
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ scout: PLAIN }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-in-use" });
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ spare: PLAIN }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-exists" });
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, rev: 1 } }), { revision: rev(f), actor: "jason", assertClean }), { code: "definition-rev-conflict" });
|
|
// The credential store's check is required on every apply, not only one that writes definitions.
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: PLAIN }), { revision: rev(f), actor: "jason" }), { code: "definition-secret-check-required" });
|
|
for (const bare of [{ board: BOARD }, { workflows: WORKFLOWS }, { "projects.stack.board": BOARD }, { packs: { ...packPlan().packs, proposedHires: [] }, workflows: WORKFLOWS }]) {
|
|
assert.throws(() => f.writer.applyPack("acme", bare, { revision: rev(f), actor: "jason" }), { code: "definition-secret-check-required" }, Object.keys(bare).join());
|
|
}
|
|
// A secret in workflow text, a plan key or an inline proposal's entry refuses before anything is written.
|
|
const leak = { ...WORKFLOWS.change, description: SECRET };
|
|
assert.throws(() => f.writer.applyPack("acme", { workflows: { change: leak } }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
|
|
assert.throws(() => f.writer.applyPack("acme", { [SECRET]: BOARD }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, instructions: `token ${SECRET}\n` } }), { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: { ...PLAIN, role: { ...PLAIN.role, tools: ["no-such-tool"] } } }), { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
|
|
// checkComposition runs: a hired composition never holds role.hire.
|
|
const hiring = { ...PLAIN, role: { ...PLAIN.role, authority: { withinRole: [], crossRole: ["role.hire"] } } };
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ fresh: hiring }), { revision: rev(f), actor: "jason", assertClean }));
|
|
// A definition needs a packs entry, and a {definition, rev} pin names a definition this apply writes at rev 1.
|
|
assert.throws(() => f.writer.applyPack("acme", { definitions: { fresh: PLAIN } }, { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
const pin = (p) => ({ ...plan({ fresh: PLAIN }), packs: { ...packPlan().packs, proposedHires: [{ name: "x-1", pin: p }] } });
|
|
assert.throws(() => f.writer.applyPack("acme", pin({ definition: "spare", rev: 2 }), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
assert.throws(() => f.writer.applyPack("acme", pin({ definition: "fresh", rev: 2 }), { revision: rev(f), actor: "jason", assertClean }), { code: "invalid-request" });
|
|
// A held secret in a board label or workflow text refuses when the caller passes the check.
|
|
assert.throws(() => f.writer.applyPack("acme", { board: { ...BOARD, columns: [{ state: "todo", label: SECRET }, ...BOARD.columns.slice(1)] } }, { revision: rev(f), actor: "jason", assertClean }), /credential-leak/);
|
|
assert.equal(bytes(f), original);
|
|
f.writer.applyPack("acme", pin({ definition: "fresh", rev: 1 }), { revision: rev(f), actor: "jason", assertClean });
|
|
assert.deepEqual(doc(f).definitions.fresh, { ...PLAIN, rev: 1 });
|
|
});
|
|
|
|
test("SP3 a pack apply only adds, and its scope fences the project keys", (t) => {
|
|
const f = fixture(t, (d) => ({ ...d, board: BOARD, workflows: WORKFLOWS }));
|
|
const original = bytes(f);
|
|
// No definitions in these plans, so no {definition, rev} pin: each refusal below is the one named.
|
|
const packs = { ...packPlan().packs, proposedHires: PACK.proposedHires.slice(0, 1) };
|
|
const go = (plan) => f.writer.applyPack("acme", plan, { revision: rev(f), actor: "jason", assertClean });
|
|
// Nothing is overwritten or removed by an apply.
|
|
assert.throws(() => go({ packs, board: LIST_BOARD }), { code: "invalid-request" });
|
|
assert.throws(() => go({ packs, workflows: { change: WORKFLOWS.change } }), { code: "invalid-request" });
|
|
assert.throws(() => go({ packs, workflows: { other: null } }), { code: "invalid-request" });
|
|
// The writer stamps at, by and added.
|
|
for (const key of ["at", "by", "added"]) assert.throws(() => go({ packs: { ...packs, [key]: PACK[key] }, workflows: { other: WORKFLOWS.change } }), { code: "invalid-request" }, key);
|
|
// An org pack writes no project key; a project pack writes only its project's keys.
|
|
assert.throws(() => go({ packs, "projects.stack.board": BOARD }), { code: "invalid-request" });
|
|
const projectPack = { ...packs, scope: "project", project: "stack" };
|
|
assert.throws(() => go({ packs: projectPack, workflows: { other: WORKFLOWS.change } }), { code: "invalid-request" });
|
|
assert.throws(() => go({ packs: { ...projectPack, project: "nowhere" }, "projects.nowhere.board": BOARD }), { code: "invalid-request" });
|
|
assert.throws(() => go({ "projects.nowhere.board": BOARD }), { code: "invalid-request" });
|
|
assert.equal(bytes(f), original);
|
|
go({ packs: projectPack, "projects.stack.board": BOARD, "projects.stack.workflows": WORKFLOWS });
|
|
assert.deepEqual(doc(f).packs[0].added, ["projects.stack.board", "projects.stack.workflows.change"]);
|
|
assert.deepEqual(doc(f).projects.stack.board, BOARD);
|
|
});
|
|
|
|
test("SP3 the generic writer still refuses project board changes", (t) => {
|
|
const f = fixture(t);
|
|
const next = structuredClone(f.document);
|
|
next.projects.stack.board = BOARD;
|
|
assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-projects-protected" });
|
|
});
|
|
|
|
test("SP3 an inline proposal keeps its full role entry beside the pin, and the pin is the entry's hash", (t) => {
|
|
const f = fixture(t);
|
|
const inline = PACK.proposedHires[2];
|
|
// The loaded organization keeps the entry, so the raise can read it back.
|
|
const loaded = load(f, { ...f.document, packs: [{ ...PACK, proposedHires: [inline] }] });
|
|
assert.deepEqual(loaded.packs[0].proposedHires[0], inline);
|
|
// A recorded hash that no longer matches still loads: staleness is the raise's refusal.
|
|
const stale = { ...inline, pin: { ...inline.pin, sha256: HASH } };
|
|
assert.deepEqual(load(f, { ...f.document, packs: [{ ...PACK, proposedHires: [stale] }] }).packs[0].proposedHires[0], stale);
|
|
// An apply records the entry, and refuses a pin that is not the entry's canonical hash.
|
|
const original = bytes(f);
|
|
const plan = (hire) => ({ packs: { ...packPlan().packs, proposedHires: [hire] }, workflows: WORKFLOWS });
|
|
assert.throws(() => f.writer.applyPack("acme", plan(stale), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "invalid-request" });
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ ...inline, entry: { ...WRITER, instructions: "Something else.\n" } }), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), { code: "invalid-request" });
|
|
// A held secret in the entry refuses when the caller passes the check.
|
|
const leaky = { ...WRITER, instructions: SECRET };
|
|
assert.throws(() => f.writer.applyPack("acme", plan({ ...inline, pin: { ...inline.pin, sha256: sha256(canonicalJson(leaky)) }, entry: leaky }), { revision: rev(f), actor: "chief-of-staff.chief", assertClean }), /credential-leak/);
|
|
assert.equal(bytes(f), original);
|
|
// An agent apply writes no definition, so its hires are template or inline pins.
|
|
f.writer.applyPack("acme", plan(inline), { revision: rev(f), actor: "chief-of-staff.chief", assertClean });
|
|
assert.deepEqual(doc(f).packs[0].proposedHires, [inline]);
|
|
assert.equal(doc(f).definitions?.writer, undefined);
|
|
assert.deepEqual(load(f, doc(f)).packs[0].proposedHires, [inline]);
|
|
});
|