366 lines
23 KiB
JavaScript
366 lines
23 KiB
JavaScript
// #1873 T1: organization definitions and templates (roles-and-rails design sections 4, 5 and 13).
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import * as fs from "node:fs";
|
|
import { syncBuiltinESMExports } from "node:module";
|
|
import { appendFileSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { organizationWriter } from "../src/organization.mjs";
|
|
import { validateBusinessDocument } from "../src/organization-config.mjs";
|
|
import { definitionVerbs, DEFINITION_VERBS } from "../src/definition-verbs.mjs";
|
|
import { readTemplate, templateHash, templateHashes, diffDefinition, lineDiff } from "../src/definitions.mjs";
|
|
import { resolveInstance } from "../src/resolve.mjs";
|
|
import { ROLE_RIGHTS, VERB_RIGHTS, normalizeRights } from "../src/org-rights.mjs";
|
|
import { businessDoc, rolesCopy, systemFor, tmp, writeJson } from "./helpers.mjs";
|
|
|
|
const SECRET = "sk-test-NOT-A-SECRET-1234";
|
|
// The credential store's check, as if it held SECRET.
|
|
const assertClean = (value) => {
|
|
if (String(value).includes(SECRET)) throw new Error(`credential-leak ${value}`);
|
|
};
|
|
const OWNER = { actor: "jason", rights: [...ROLE_RIGHTS.owner], agent: false };
|
|
const MEMBER = { actor: "alice", rights: [...ROLE_RIGHTS.member], agent: false };
|
|
const HIRER = { actor: "alice", rights: normalizeRights(ROLE_RIGHTS.member, { add: ["org.hire"] }), agent: false };
|
|
const PLAIN = { role: { tools: ["read"], network: "none", authority: { withinRole: [], crossRole: [] }, credentials: [] }, instructions: "Read and report.\n" };
|
|
|
|
function fixture(t, edit = (doc) => doc) {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const dir = join(root, "config");
|
|
const rolesDir = rolesCopy(root);
|
|
const file = writeJson(join(dir, "businesses/acme.json"), edit(businessDoc(root)));
|
|
const writer = organizationWriter({ dir, rolesDir });
|
|
return { root, dir, file, rolesDir, writer, verbs: definitionVerbs({ writer, rolesDir, assertClean }) };
|
|
}
|
|
const rev = (f) => f.writer.read("acme").revision;
|
|
const doc = (f) => JSON.parse(readFileSync(f.file, "utf8"));
|
|
|
|
test("T1 import pins the template hash and copies its primitives and contract", (t) => {
|
|
const f = fixture(t);
|
|
const hash = templateHash(f.rolesDir, "coder");
|
|
assert.match(hash, /^[0-9a-f]{64}$/);
|
|
const receipt = f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
|
|
assert.deepEqual(Object.keys(receipt).sort(), ["definition", "from", "organization", "rev", "revision", "transaction", "verb"]);
|
|
assert.equal(receipt.rev, 1);
|
|
assert.deepEqual(receipt.from, { template: "coder", sha256: hash });
|
|
const copy = doc(f).definitions.coder;
|
|
assert.deepEqual(copy.from, { template: "coder", sha256: hash });
|
|
assert.equal(copy.rev, 1);
|
|
assert.equal(copy.instructions, readFileSync(join(f.rolesDir, "coder.md"), "utf8"));
|
|
const template = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
|
|
assert.deepEqual(copy.role.tools, template.tools);
|
|
assert.equal(copy.role.network, template.network);
|
|
assert.equal(receipt.revision, rev(f));
|
|
// The hash read lists every version 2 template, and only those.
|
|
const hashes = templateHashes(f.rolesDir);
|
|
assert.ok(hashes.some((h) => h.template === "coder" && h.sha256 === hash));
|
|
assert.ok(!hashes.some((h) => h.template === "conductor-policy"));
|
|
// --as names the copy.
|
|
const second = f.verbs.import("acme", { revision: rev(f), template: "coder", as: "coder-two" }, OWNER);
|
|
assert.equal(second.definition, "coder-two");
|
|
assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER), { code: "definition-exists" });
|
|
});
|
|
|
|
test("T1 the hash covers the template JSON and its contract together", (t) => {
|
|
const f = fixture(t);
|
|
const before = templateHash(f.rolesDir, "coder");
|
|
appendFileSync(join(f.rolesDir, "coder.md"), "\nOne more line.\n");
|
|
const contractChanged = templateHash(f.rolesDir, "coder");
|
|
assert.notEqual(contractChanged, before);
|
|
const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
|
|
writeJson(join(f.rolesDir, "coder.json"), { ...json, title: `${json.title} (renamed)` }, 0o644);
|
|
assert.notEqual(templateHash(f.rolesDir, "coder"), contractChanged);
|
|
});
|
|
|
|
test("T1 editing a template file changes no copy, and the diff read shows the change", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
|
|
const bytes = readFileSync(f.file);
|
|
const pinned = doc(f).definitions.coder.from.sha256;
|
|
assert.deepEqual(f.verbs.diff("acme", "coder"), { organization: "acme", revision: rev(f), definition: "coder", rev: 1, from: { template: "coder", sha256: pinned }, current: pinned, changed: false, primitives: [], instructions: [] });
|
|
|
|
appendFileSync(join(f.rolesDir, "coder.md"), "Always run the suites twice.\n");
|
|
const json = JSON.parse(readFileSync(join(f.rolesDir, "coder.json"), "utf8"));
|
|
writeJson(join(f.rolesDir, "coder.json"), { ...json, network: "none" }, 0o644);
|
|
assert.notEqual(json.network, "none", "the fixture must change network");
|
|
|
|
// No copy changed, and the file still loads.
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
assert.equal(f.writer.read("acme").document.definitions.coder.from.sha256, pinned);
|
|
|
|
const diff = f.verbs.diff("acme", "coder");
|
|
assert.equal(diff.changed, true);
|
|
assert.equal(diff.current, templateHash(f.rolesDir, "coder"));
|
|
assert.notEqual(diff.current, pinned);
|
|
assert.deepEqual(diff.primitives, ["network", "instructions"]);
|
|
assert.deepEqual(diff.instructions.filter((l) => l.op !== " "), [{ op: "+", line: "Always run the suites twice." }]);
|
|
// Nothing applied.
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
});
|
|
|
|
test("T1 diff read: an invented definition has nothing to compare, a missing template is named", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
assert.equal(f.verbs.diff("acme", "scout").changed, false);
|
|
assert.equal(f.verbs.diff("acme", "scout").current, null);
|
|
f.verbs.import("acme", { revision: rev(f), template: "proofreader" }, OWNER);
|
|
rmSync(join(f.rolesDir, "proofreader.json"));
|
|
const gone = diffDefinition(f.writer.read("acme").document, "proofreader", f.rolesDir);
|
|
assert.equal(gone.templateMissing, true);
|
|
assert.throws(() => f.verbs.diff("acme", "nobody"), { name: "BusinessError" });
|
|
assert.deepEqual(lineDiff("a\nb", "a\nc"), [{ op: " ", line: "a" }, { op: "-", line: "b" }, { op: "+", line: "c" }]);
|
|
});
|
|
|
|
test("T1 rev counts up on every write, and a stale write refuses", (t) => {
|
|
const f = fixture(t);
|
|
assert.equal(f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER).rev, 1);
|
|
const edited = { ...PLAIN, instructions: "Read, then report.\n" };
|
|
assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: edited }, OWNER).rev, 2);
|
|
assert.equal(f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER).rev, 3);
|
|
assert.equal(doc(f).definitions.scout.rev, 3);
|
|
const bytes = readFileSync(f.file);
|
|
// A stale rev, a stale file revision, an unchanged edit.
|
|
assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 2, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-conflict" });
|
|
const old = rev(f);
|
|
assert.throws(() => f.verbs.edit("acme", { revision: "0".repeat(64), rev: 3, name: "scout", definition: PLAIN }, OWNER), { code: "revision-conflict" });
|
|
assert.throws(() => f.verbs.edit("acme", { revision: old, rev: 3, name: "scout", definition: { ...edited, personality: { summary: "Terse." } } }, OWNER), { code: "definition-unchanged" });
|
|
assert.throws(() => f.verbs.edit("acme", { revision: old, name: "scout", definition: PLAIN }, OWNER), { code: "definition-rev-required" });
|
|
assert.throws(() => f.verbs.remove("acme", { revision: old, rev: 1, name: "scout" }, OWNER), { code: "definition-rev-conflict" });
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
// Edit keeps from; remove takes the entry out.
|
|
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
|
|
const from = doc(f).definitions.coder.from;
|
|
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER);
|
|
assert.deepEqual(doc(f).definitions.coder.from, from);
|
|
const removed = f.verbs.remove("acme", { revision: rev(f), rev: 3, name: "scout" }, OWNER);
|
|
assert.equal(removed.removedRev, 3);
|
|
assert.ok(!Object.hasOwn(doc(f).definitions, "scout"));
|
|
// Each write is logged with its verb, name and rev, never role text.
|
|
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed");
|
|
assert.deepEqual(log.map((e) => [e.detail.verb, e.detail.definition, e.detail.rev]), [
|
|
["definition.invent.console", "scout", 1], ["definition.edit.console", "scout", 2], ["definition.edit.console", "scout", 3],
|
|
["definition.import.console", "coder", 1], ["definition.edit.console", "coder", 2], ["definition.remove.console", "scout", null],
|
|
]);
|
|
});
|
|
|
|
test("T1 invent and customize refuse what checkComposition refuses, with no value in the error", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
const bytes = readFileSync(f.file);
|
|
const bad = [
|
|
["held credential in instructions", { ...PLAIN, instructions: `Use ${SECRET} to log in.\n` }],
|
|
["held credential in personality", { ...PLAIN, personality: { voice: `say ${SECRET}` } }],
|
|
["unregistered tool", { ...PLAIN, role: { ...PLAIN.role, tools: [SECRET] } }],
|
|
["unregistered service", { ...PLAIN, role: { ...PLAIN.role, credentials: [{ service: SECRET }] } }],
|
|
["hire chain", { ...PLAIN, role: { ...PLAIN.role, authority: { withinRole: ["role.hire"], crossRole: [] } } }],
|
|
["over bound", { ...PLAIN, instructions: "x".repeat(16385) }],
|
|
["unknown entry key", { ...PLAIN, [SECRET]: 1 }],
|
|
["unknown role key", { ...PLAIN, role: { ...PLAIN.role, [SECRET]: [] } }],
|
|
];
|
|
for (const [what, definition] of bad) {
|
|
for (const call of [
|
|
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition }, OWNER),
|
|
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition }, OWNER),
|
|
]) {
|
|
assert.throws(call, (e) => !e.message.includes(SECRET) && !e.message.includes("x".repeat(64)) && !String(e.stack).includes(SECRET), what);
|
|
}
|
|
}
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
// Import refuses a template that holds a hire right (R9): the chief of staff.
|
|
assert.throws(() => f.verbs.import("acme", { revision: rev(f), template: "chief-of-staff" }, OWNER), /cannot hire or fire/);
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
});
|
|
|
|
test("T1 a bad entry in the file refuses the whole file, naming the definition and key, never the value", (t) => {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const rolesDir = rolesCopy(root);
|
|
const file = join(root, "config/businesses/acme.json");
|
|
const base = businessDoc(root);
|
|
const entry = { rev: 1, ...PLAIN };
|
|
const refusal = (definitions) => {
|
|
try {
|
|
validateBusinessDocument({ ...base, definitions }, file, { rolesDir, assertClean });
|
|
} catch (e) {
|
|
assert.equal(e.name, "BusinessError");
|
|
return e.message;
|
|
}
|
|
assert.fail("expected a refusal");
|
|
};
|
|
assert.match(refusal({ scout: { ...entry, instructions: `see ${SECRET}` } }), /definitions\.scout: composition instructions holds a credential/);
|
|
assert.match(refusal({ scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }), /definitions\.scout: .*tools/);
|
|
assert.match(refusal({ scout: { ...entry, rev: 0 } }), /definitions\.scout\.rev must be a positive integer/);
|
|
assert.match(refusal({ scout: { ...entry, from: { template: "coder", sha256: SECRET } } }), /definitions\.scout\.from\.sha256/);
|
|
assert.match(refusal({ scout: { ...entry, [SECRET]: true } }), /definitions\.scout has an unknown key \(key \d+;/);
|
|
assert.match(refusal({ [SECRET]: entry }), /definitions key 1 must be a definition name/);
|
|
for (const d of [{ scout: { ...entry, instructions: `see ${SECRET}` } }, { scout: { ...entry, role: { ...PLAIN.role, tools: [SECRET] } } }, { [SECRET]: entry }, { scout: { ...entry, [SECRET]: true } }]) {
|
|
assert.ok(!refusal(d).includes(SECRET));
|
|
}
|
|
// A good definition validates; the resolved map holds only what instances name.
|
|
const ok = validateBusinessDocument({ ...base, definitions: { scout: entry } }, file, { rolesDir });
|
|
assert.ok(!Object.hasOwn(ok.definitions, "org:scout"));
|
|
});
|
|
|
|
test("T1 an org: reference resolves to the definition, and a missing one refuses naming instance and definition", (t) => {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const rolesDir = rolesCopy(root);
|
|
const file = join(root, "config/businesses/acme.json");
|
|
const base = businessDoc(root);
|
|
const named = { ...base, definitions: { scout: { rev: 4, ...PLAIN, personality: { summary: "Terse." } } }, roles: { ...base.roles, helper: { definition: "org:scout" } } };
|
|
const business = validateBusinessDocument(named, file, { rolesDir });
|
|
const role = business.definitions["org:scout"];
|
|
assert.equal(role.name, "org:scout");
|
|
assert.equal(role.contractPath, null);
|
|
assert.equal(role.instructions, PLAIN.instructions);
|
|
assert.equal(role.rev, 4);
|
|
const resolved = resolveInstance({ system: systemFor(root), business, instance: "helper" });
|
|
assert.equal(resolved.definition, "org:scout");
|
|
assert.equal(resolved.contract, null);
|
|
assert.equal(resolved.instructions, PLAIN.instructions);
|
|
assert.deepEqual(resolved.limits.tools, ["read"]);
|
|
|
|
const missing = { ...base, roles: { ...base.roles, helper: { definition: "org:ghost" } } };
|
|
assert.throws(() => validateBusinessDocument(missing, file, { rolesDir }), (e) => /definition-missing: .*roles\.helper\.definition names org:ghost/.test(e.message));
|
|
assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: `org:${SECRET.toLowerCase()}!` } } }, file, { rolesDir }), (e) => !e.message.includes(SECRET.toLowerCase()));
|
|
// Template names work exactly as before, including one shaped like a definition name.
|
|
assert.throws(() => validateBusinessDocument({ ...base, roles: { ...base.roles, helper: { definition: "scout" } } }, file, { rolesDir }), { name: "BusinessError" });
|
|
});
|
|
|
|
test("T1 an org whose instances name only templates loads exactly as before", (t) => {
|
|
const root = tmp();
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const rolesDir = rolesCopy(root);
|
|
const file = join(root, "config/businesses/acme.json");
|
|
const base = businessDoc(root);
|
|
const plain = validateBusinessDocument(base, file, { rolesDir });
|
|
const withUnused = validateBusinessDocument({ ...base, definitions: { scout: { rev: 1, ...PLAIN } } }, file, { rolesDir });
|
|
assert.deepEqual(withUnused, plain);
|
|
assert.deepEqual(Object.keys(plain).sort(), ["arbiters", "businessVersion", "definitions", "file", "human", "id", "launch", "plugins", "projects", "roles", "tracker", "vars"]);
|
|
assert.deepEqual(Object.keys(plain.definitions).sort(), ["chief-of-staff", "coder", "cto", "pm", "reviewer"]);
|
|
const system = systemFor(root);
|
|
for (const instance of Object.keys(base.roles)) {
|
|
const a = resolveInstance({ system, business: plain, instance });
|
|
const b = resolveInstance({ system, business: withUnused, instance });
|
|
assert.equal(a.digest, b.digest);
|
|
assert.ok(!Object.hasOwn(a, "instructions"));
|
|
assert.equal(a.contract, plain.definitions[base.roles[instance].definition].contractPath);
|
|
}
|
|
});
|
|
|
|
test("T1 definition-in-use: an edit or removal of a definition an instance names refuses, naming it", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
// A human adds the instance through the ordinary writer; definitions stay as they are.
|
|
const next = doc(f);
|
|
next.roles.helper = { definition: "org:scout" };
|
|
f.writer.write("acme", next, { revision: rev(f), actor: "jason" });
|
|
const bytes = readFileSync(f.file);
|
|
assert.throws(() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Changed.\n" } }, OWNER),
|
|
(e) => e.code === "definition-in-use" && /named by helper/.test(e.message));
|
|
assert.throws(() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER),
|
|
(e) => e.code === "definition-in-use" && /named by helper/.test(e.message));
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
assert.deepEqual(f.verbs.list("acme").definitions, [{ name: "scout", reference: "org:scout", rev: 1, from: null, usedBy: ["helper"] }]);
|
|
assert.deepEqual(f.verbs.show("acme", "scout").usedBy, ["helper"]);
|
|
});
|
|
|
|
test("T1 the ordinary writer cannot change definitions, and setDefinition changes nothing else", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
const next = doc(f);
|
|
next.definitions.scout.instructions = "Sneaked in.\n";
|
|
next.definitions.scout.rev = 2;
|
|
assert.throws(() => f.writer.write("acme", next, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" });
|
|
const added = doc(f);
|
|
added.definitions.other = { rev: 1, ...PLAIN };
|
|
assert.throws(() => f.writer.write("acme", added, { revision: rev(f), actor: "jason" }), { code: "organization-definitions-protected" });
|
|
// The writer itself checks rev and the composition, whatever builds the entry.
|
|
assert.throws(() => f.writer.setDefinition("acme", "scout", () => ({ ...PLAIN, role: { ...PLAIN.role, tools: ["no-such-tool"] } }), { revision: rev(f), actor: "jason", assertClean }), { name: "BusinessError" });
|
|
});
|
|
|
|
test("T1 an agent cannot call the edit verbs", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
const bytes = readFileSync(f.file);
|
|
const calls = (caller) => [
|
|
() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, caller),
|
|
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, caller),
|
|
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, caller),
|
|
() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, caller),
|
|
];
|
|
// An agent session, even one holding every right; an agent's name or the tester as actor.
|
|
for (const caller of [
|
|
{ ...OWNER, agent: true }, { actor: "coder", rights: OWNER.rights }, { ...OWNER, actor: "coder" },
|
|
{ ...OWNER, actor: "coder.coder" }, { ...OWNER, actor: "tester" }, { ...OWNER, actor: "chief" },
|
|
]) {
|
|
for (const call of calls(caller)) assert.throws(call, { code: "human-required" });
|
|
}
|
|
// The writer refuses an agent actor by itself too.
|
|
assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "pm" }), { code: "human-required" });
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
});
|
|
|
|
test("T1 a member without org.hire refuses on each verb; the owner and a member granted org.hire succeed", (t) => {
|
|
const f = fixture(t);
|
|
for (const verb of Object.values(DEFINITION_VERBS)) assert.equal(VERB_RIGHTS[verb], "org.hire");
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
const bytes = readFileSync(f.file);
|
|
const refused = [
|
|
() => f.verbs.import("acme", { revision: rev(f), template: "coder" }, MEMBER),
|
|
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, MEMBER),
|
|
() => f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, MEMBER),
|
|
() => f.verbs.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, MEMBER),
|
|
() => f.verbs.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, { actor: "alice", agent: false }),
|
|
];
|
|
for (const call of refused) assert.throws(call, (e) => e.code === "right-required" && /needs org\.hire/.test(e.message));
|
|
assert.deepEqual(readFileSync(f.file), bytes);
|
|
// Owner: every verb.
|
|
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
|
|
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "x\n" } }, OWNER);
|
|
f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, OWNER);
|
|
// A member granted org.hire: every verb, logged as that member.
|
|
f.verbs.import("acme", { revision: rev(f), template: "reviewer" }, HIRER);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, HIRER);
|
|
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "y\n" } }, HIRER);
|
|
f.verbs.remove("acme", { revision: rev(f), rev: 2, name: "scout" }, HIRER);
|
|
const log = readFileSync(f.writer.logFile("acme"), "utf8").trim().split("\n").map(JSON.parse).filter((e) => e.state === "committed");
|
|
assert.deepEqual(log.slice(-4).map((e) => e.actor), ["alice", "alice", "alice", "alice"]);
|
|
});
|
|
|
|
test("T1 the template files on disk are never written", (t) => {
|
|
const f = fixture(t);
|
|
const before = readFileSync(join(f.rolesDir, "coder.json"));
|
|
f.verbs.import("acme", { revision: rev(f), template: "coder" }, OWNER);
|
|
f.verbs.edit("acme", { revision: rev(f), rev: 1, name: "coder", definition: PLAIN }, OWNER);
|
|
assert.deepEqual(readFileSync(join(f.rolesDir, "coder.json")), before);
|
|
writeFileSync(join(f.root, "touched"), "");
|
|
});
|
|
|
|
test("T1 a write that adds or changes a definition refuses without the credential store's secret check", (t) => {
|
|
const f = fixture(t);
|
|
f.verbs.invent("acme", { revision: rev(f), name: "scout", definition: PLAIN }, OWNER);
|
|
const unchecked = definitionVerbs({ writer: f.writer, rolesDir: f.rolesDir });
|
|
const before = readFileSync(f.file);
|
|
const required = { code: "definition-secret-check-required" };
|
|
assert.throws(() => unchecked.import("acme", { revision: rev(f), template: "coder" }, OWNER), required);
|
|
assert.throws(() => unchecked.invent("acme", { revision: rev(f), name: "other", definition: PLAIN }, OWNER), required);
|
|
assert.throws(() => unchecked.edit("acme", { revision: rev(f), rev: 1, name: "scout", definition: { ...PLAIN, instructions: "Report.\n" } }, OWNER), required);
|
|
assert.throws(() => f.writer.setDefinition("acme", "other", () => PLAIN, { revision: rev(f), actor: "jason" }), required);
|
|
assert.deepEqual(readFileSync(f.file), before);
|
|
// A removal writes no role text, so it needs no secret check.
|
|
assert.equal(unchecked.remove("acme", { revision: rev(f), rev: 1, name: "scout" }, OWNER).removedRev, 1);
|
|
});
|
|
|
|
test('A4 T1 hand-off: a failed second contract read names the template, never the path', t => {
|
|
const root=tmp(),rolesDir=rolesCopy(root),read=fs.default.readFileSync;
|
|
let contracts=0;
|
|
fs.default.readFileSync=function(file,...args){
|
|
if(file===join(rolesDir,'reviewer.md') && ++contracts===2)throw new Error(`cannot read ${root}/sk-test-NOT-A-SECRET-1234`);
|
|
return read(file,...args);
|
|
};
|
|
syncBuiltinESMExports();
|
|
try { assert.throws(()=>readTemplate(rolesDir,'reviewer'),{message:'template reviewer contract is unavailable'}); }
|
|
finally { fs.default.readFileSync=read;syncBuiltinESMExports();rmSync(root,{recursive:true,force:true}); }
|
|
});
|