68 lines
3.7 KiB
JavaScript
68 lines
3.7 KiB
JavaScript
// PRD 1.5 REQ-WAIT-1 to REQ-WAIT-3 (row 139, #1633): the hard waits in the
|
|
// closed vocabulary, and the role validator's refusal of a grant of one.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { ACTIONS, GATED_ONLY, ROUTED, THREAD_DEFINITION, END_PROTECTED } from "../src/vocabulary.mjs";
|
|
import { validateRoleDocument } from "../src/index.mjs";
|
|
import { tmp } from "./helpers.mjs";
|
|
|
|
const WAITS = ["spend", "data.delete", "network.expose", "account.create", "credential.mint", "message.external",
|
|
"git.push.protected", "deploy", "git.push.remote", "host.sudo", "project.bootstrap"];
|
|
const LEFT = ["git.merge.protected", "policy.change", "prd.approve", "role.revoke"];
|
|
const role = (authority) => {
|
|
const dir = tmp();
|
|
writeFileSync(join(dir, "r.md"), "# contract\n\n## Authority\n");
|
|
const doc = { roleVersion: 2, name: "r", title: "R", contract: "r.md", tools: ["read"], network: "none", authority,
|
|
credentials: [{ service: "gitea", scopes: ["read:issue"] }] };
|
|
return validateRoleDocument(doc, join(dir, "r.json"));
|
|
};
|
|
|
|
test("REQ-WAIT-1: GATED_ONLY is exactly the hard waits; the new actions are in the vocabulary", () => {
|
|
assert.deepEqual([...GATED_ONLY].sort(), [...WAITS].sort());
|
|
for (const action of ["data.delete", "network.expose", "account.create"]) assert.ok(ACTIONS.includes(action), action);
|
|
for (const action of LEFT) assert.ok(ACTIONS.includes(action) && !GATED_ONLY.includes(action), action);
|
|
});
|
|
|
|
// Row 155 (#1649): the broker and the organization writer read both from here, so the assistant
|
|
// can't lose its end protection by an edit to one list.
|
|
test("REQ-ROLE-2: END_PROTECTED includes the assistant's thread definition", () => {
|
|
assert.equal(THREAD_DEFINITION, "assistant");
|
|
assert.ok(END_PROTECTED.includes(THREAD_DEFINITION));
|
|
assert.ok(Object.isFrozen(END_PROTECTED));
|
|
});
|
|
|
|
// PRD 1.7 REQ-COS-3 (row 194, #1692): the chief's hire and fire left the hard waits and are routed.
|
|
test("REQ-COS-3: role.hire and role.fire are routed, not hard waits", () => {
|
|
for (const action of ["role.hire", "role.fire"]) assert.ok(ROUTED.includes(action) && !GATED_ONLY.includes(action), action);
|
|
});
|
|
|
|
// PRD 1.16 REQ-WAIT-3 and REQ-LIB-6 (row 285, #1790): library.write is in the vocabulary and routed.
|
|
test("REQ-WAIT-3: library.write is routed under REQ-LIB-6 (1.16), not a hard wait", () => {
|
|
assert.ok(ACTIONS.includes("library.write"));
|
|
assert.ok(ROUTED.includes("library.write"));
|
|
assert.ok(!GATED_ONLY.includes("library.write"));
|
|
});
|
|
|
|
test("REQ-WAIT-3: every action is routed or a hard wait, never both, and the routed list names only actions", () => {
|
|
for (const action of ROUTED) assert.ok(ACTIONS.includes(action), action);
|
|
for (const action of ACTIONS) assert.notEqual(ROUTED.includes(action), GATED_ONLY.includes(action), action);
|
|
});
|
|
|
|
test("REQ-WAIT-3: instance.name is routed under REQ-MSG-2 (1.5), not a hard wait (row 141, sage-0589)", () => {
|
|
assert.ok(ACTIONS.includes("instance.name"));
|
|
assert.ok(ROUTED.includes("instance.name"));
|
|
assert.ok(!GATED_ONLY.includes("instance.name"));
|
|
});
|
|
|
|
test("REQ-WAIT-2: a role listing a hard wait at either class is refused; the actions that left can be granted", () => {
|
|
for (const action of WAITS) {
|
|
assert.throws(() => role({ withinRole: [action], crossRole: [] }), /always gated/, action);
|
|
assert.throws(() => role({ withinRole: [], crossRole: [action] }), /always gated/, action);
|
|
}
|
|
assert.deepEqual(role({ withinRole: [], crossRole: ["git.merge.protected"] }).authority.crossRole,
|
|
["git.merge.protected"]);
|
|
assert.deepEqual(role({ withinRole: LEFT.slice(1), crossRole: [] }).authority.withinRole, LEFT.slice(1));
|
|
});
|