feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)
Rocko-authored, Filbert-reviewed inspector (r6 manifest a4a44930...) with full review/build/verdict evidence under docs/plans/reviews. 43/0 selftests, oracle zero-disagreement, foundation checker PASS. Owner A9 acceptance recorded separately.
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
// Unit tests for the pure evaluation core (charter §4, §7, §8, §10.4).
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
findCycle, deepEqual, isValidTime, isValidRelativePath,
|
||||
intersectGrants, pathPermitted, layerFromRestrictions, UNRESTRICTED_LAYER, intersectLayers,
|
||||
buildResult, exitFor, Refusal, RULES, REASONS, RESULTS, PROPOSAL_STEPS, SUPPORTED_OPERATIONS, OPERATION_CATALOG,
|
||||
validateRecordShape, evaluate, inspect, DISCLAIMER, PREVIEW, RESULT_KEYS as IMPL_RESULT_KEYS,
|
||||
PINNED_PATTERNS, pinnedPatternMatches, strictPatternMatches, stableOrder, orderingForm, ORDER_KEYS,
|
||||
} from "./resolve.mjs";
|
||||
import { parseStrict } from "./strict-json.mjs";
|
||||
import { canonicalize, CanonicalError } from "./canonical.mjs";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const FIXTURES = join(HERE, "fixtures");
|
||||
const CHARTER = join(HERE, "..", "..", "docs", "plans", "2026-09-06_foundation-inspector-charter.md");
|
||||
const CHARTER_SHA256 = "19b6721128a627a2032ffdb95ece2d50abe69a8f6d521e9eff8bbdaff22798b6";
|
||||
const INDEX = JSON.parse(readFileSync(join(FIXTURES, "index.json"), "utf8"));
|
||||
|
||||
function loadBundle(name) {
|
||||
return parseStrict(new Uint8Array(readFileSync(join(FIXTURES, "bundles", `${name}.json`))));
|
||||
}
|
||||
|
||||
const CLOSED_KEYS = new Set([
|
||||
"disclaimer", "preview", "bundleVersion", "authentication", "declarations", "result", "reason", "rule",
|
||||
"selection", "operation", "proposal", "diagnostic",
|
||||
"agentId", "projectId", "workspaceId", "assignmentRef", "kind", "id", "scope", "revision",
|
||||
"name", "target", "root", "path", "selectedAssignmentRef", "byteOffset", "inputPath",
|
||||
]);
|
||||
|
||||
/** Every key at any depth that is not part of the closed result vocabulary (§7). */
|
||||
function unknownKeys(value, out = []) {
|
||||
if (Array.isArray(value)) { for (const v of value) unknownKeys(v, out); return out; }
|
||||
if (value && typeof value === "object") {
|
||||
for (const [k, v] of Object.entries(value)) { if (!CLOSED_KEYS.has(k)) out.push(k); unknownKeys(v, out); }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Independent copy of the charter §7 closed field list (not the implementation export).
|
||||
const RESULT_KEYS = [
|
||||
"disclaimer", "preview", "bundleVersion", "authentication", "declarations", "result", "reason", "rule",
|
||||
"selection", "operation", "proposal", "diagnostic",
|
||||
];
|
||||
|
||||
/** The §7 "Closed result fields" bullet names parsed from the pinned charter text. */
|
||||
function charterResultFields() {
|
||||
const bytes = readFileSync(CHARTER);
|
||||
const digest = createHash("sha256").update(bytes).digest("hex");
|
||||
assert.equal(digest, CHARTER_SHA256, "charter candidate 3 must be the pinned text; rebind this test on amendment");
|
||||
const text = bytes.toString("utf8");
|
||||
const start = text.indexOf("Closed result fields, emitted on every outcome:");
|
||||
assert.ok(start > 0, "charter §7 field list not found");
|
||||
const block = text.slice(start, text.indexOf("\n\n", start));
|
||||
return block.split("\n").map((l) => /^- ([a-zA-Z]+):/.exec(l)).filter(Boolean).map((m) => m[1]);
|
||||
}
|
||||
|
||||
// --- graph -------------------------------------------------------------------------
|
||||
|
||||
test("findCycle: self-loop, pair, diamond, long chain without recursion", () => {
|
||||
assert.equal(findCycle(new Map([["a", ["a"]]])), "a");
|
||||
assert.equal(findCycle(new Map([["a", ["b"]], ["b", ["a"]]])), "a");
|
||||
assert.equal(findCycle(new Map([["a", ["b", "c"]], ["b", ["d"]], ["c", ["d"]], ["d", []]])), null);
|
||||
assert.equal(findCycle(new Map()), null);
|
||||
assert.equal(findCycle(new Map([["a", ["missing"]]])), null);
|
||||
const n = 20000;
|
||||
const chain = new Map();
|
||||
for (let i = 0; i < n; i += 1) chain.set(`n${String(i).padStart(6, "0")}`, [`n${String(i + 1).padStart(6, "0")}`]);
|
||||
assert.equal(findCycle(chain), null);
|
||||
chain.set(`n${String(n).padStart(6, "0")}`, ["n000000"]);
|
||||
assert.equal(findCycle(chain), "n000000");
|
||||
});
|
||||
|
||||
test("deepEqual: structural, key-order insensitive, type-strict", () => {
|
||||
assert.ok(deepEqual({ a: [1, { b: null }] }, { a: [1, { b: null }] }));
|
||||
assert.ok(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||||
assert.ok(!deepEqual({ a: 1 }, { a: "1" }));
|
||||
assert.ok(!deepEqual([1, 2], [2, 1]));
|
||||
assert.ok(!deepEqual({ a: 1 }, { a: 1, b: undefined }));
|
||||
assert.ok(!deepEqual(null, {}));
|
||||
assert.ok(!deepEqual([], {}));
|
||||
});
|
||||
|
||||
// --- formats (pinned checker semantics) -----------------------------------------------------
|
||||
|
||||
test("isValidTime follows the pinned checker's strptime round-trip", () => {
|
||||
const cases = [
|
||||
["2026-09-06T03:00:00.000Z", true], ["2026-02-30T00:00:00.000Z", false], ["2024-02-29T00:00:00.000Z", true],
|
||||
["2023-02-29T00:00:00.000Z", false], ["2026-09-06T24:00:00.000Z", false], ["2026-09-06T03:00:60.000Z", false],
|
||||
["2026-09-06T03:00:00Z", false], ["2026-09-06T03:00:00.0000Z", false], ["0000-01-01T00:00:00.000Z", false],
|
||||
["9999-12-31T23:59:59.999Z", true], ["2026-09-06T03:00:00.000Z\n", false], ["2026-9-06T03:00:00.000Z", false],
|
||||
// pinned checker (check.py strftime round-trip, glibc %Y unpadded below 1000): 0001..0999 refused
|
||||
["0001-01-01T00:00:00.000Z", false], ["0999-12-31T00:00:00.000Z", false], ["1000-01-01T00:00:00.000Z", true],
|
||||
["2026-09-06T03:00:00.000+00:00", false], ["2026-13-01T00:00:00.000Z", false], ["2100-02-29T00:00:00.000Z", false],
|
||||
["2000-02-29T00:00:00.000Z", true], [null, false], [20260906, false],
|
||||
];
|
||||
for (const [value, expected] of cases) assert.equal(isValidTime(value), expected, JSON.stringify(value));
|
||||
});
|
||||
|
||||
test("isValidRelativePath: pinned checker categories and byte bounds", () => {
|
||||
const ok = ["src/main.mjs", "a", "a/b/c", "x".repeat(4096), "é".repeat(2048), "\u2028", "spa ce", "a.b", "...", "dot./x"];
|
||||
const bad = ["", "/abs", "a//b", "./a", "a/../b", "a/.", "..", "a\\b", "a\u0001", "a\u200b", "\ud800", "a\u0085", "\u007f",
|
||||
"x".repeat(4097), "é".repeat(2049), "\ufeffx", "a/", null, 5];
|
||||
for (const v of ok) assert.ok(isValidRelativePath(v), JSON.stringify(v));
|
||||
for (const v of bad) assert.ok(!isValidRelativePath(v), JSON.stringify(v));
|
||||
});
|
||||
|
||||
// --- restrictions calculus ---------------------------------------------------------------
|
||||
|
||||
const ROOT = { root: "workspace", path: null };
|
||||
const SRC = { root: "workspace", path: "src" };
|
||||
const SRC_LIB = { root: "workspace", path: "src/lib" };
|
||||
const DOCS = { root: "workspace", path: "docs" };
|
||||
|
||||
test("grant intersection keeps the narrower prefix and never widens", () => {
|
||||
assert.deepEqual(intersectGrants([ROOT], [SRC]), [SRC]);
|
||||
assert.deepEqual(intersectGrants([SRC], [ROOT]), [SRC]);
|
||||
assert.deepEqual(intersectGrants([SRC], [SRC_LIB]), [SRC_LIB]);
|
||||
assert.deepEqual(intersectGrants([SRC], [DOCS]), []);
|
||||
assert.deepEqual(intersectGrants([SRC, DOCS], [ROOT]), [SRC, DOCS]);
|
||||
assert.deepEqual(intersectGrants([], [ROOT]), []);
|
||||
assert.deepEqual(intersectGrants([ROOT], [ROOT]), [ROOT]);
|
||||
// segment prefixes, not string prefixes
|
||||
assert.deepEqual(intersectGrants([SRC], [{ root: "workspace", path: "srcx" }]), []);
|
||||
});
|
||||
|
||||
test("pathPermitted uses segment prefixes", () => {
|
||||
assert.ok(pathPermitted("src/a.mjs", [SRC]));
|
||||
assert.ok(pathPermitted("src", [SRC]));
|
||||
assert.ok(!pathPermitted("srcx/a.mjs", [SRC]));
|
||||
assert.ok(!pathPermitted("docs/a.md", [SRC]));
|
||||
assert.ok(pathPermitted("anything/at/all", [ROOT]));
|
||||
assert.ok(!pathPermitted("anything", []));
|
||||
});
|
||||
|
||||
test("layer intersection narrows only; network none dominates; endpoints intersect structurally", () => {
|
||||
const full = layerFromRestrictions({
|
||||
operations: [...OPERATION_CATALOG], readPaths: [ROOT], writePaths: [ROOT], network: "none", endpointRefs: [],
|
||||
});
|
||||
assert.deepEqual(intersectLayers(UNRESTRICTED_LAYER, full), full);
|
||||
assert.deepEqual(intersectLayers(full, UNRESTRICTED_LAYER), full);
|
||||
const reader = layerFromRestrictions({ operations: ["work.read", "file.read"], readPaths: [SRC], writePaths: [], network: "none", endpointRefs: [] });
|
||||
const both = intersectLayers(full, reader);
|
||||
assert.deepEqual(both.operations, ["work.read", "file.read"]);
|
||||
assert.deepEqual(both.readPaths, [SRC]);
|
||||
assert.deepEqual(both.writePaths, []);
|
||||
assert.equal(both.network, "none");
|
||||
const ep1 = { registry: "endpoint", id: "e1", revision: 1, digest: `sha256:${"0".repeat(64)}` };
|
||||
const ep2 = { ...ep1, id: "e2" };
|
||||
const netA = { ...UNRESTRICTED_LAYER, network: "approved-endpoints", endpointRefs: [ep1, ep2] };
|
||||
const netB = { ...UNRESTRICTED_LAYER, network: "approved-endpoints", endpointRefs: [{ ...ep2 }] };
|
||||
const netC = { ...UNRESTRICTED_LAYER, network: "approved-endpoints", endpointRefs: [{ ...ep1, revision: 2 }] };
|
||||
assert.deepEqual(intersectLayers(netA, netB), { ...UNRESTRICTED_LAYER, network: "approved-endpoints", endpointRefs: [ep2] });
|
||||
assert.deepEqual(intersectLayers(netA, netC), { ...UNRESTRICTED_LAYER, network: "none", endpointRefs: [] });
|
||||
assert.deepEqual(intersectLayers(netA, { ...UNRESTRICTED_LAYER, network: "none", endpointRefs: [] }), { ...UNRESTRICTED_LAYER, network: "none", endpointRefs: [] });
|
||||
// delegated operation set narrows without touching paths
|
||||
const delegated = { ...UNRESTRICTED_LAYER, operations: ["assignment.change"] };
|
||||
const narrowed = intersectLayers(full, delegated);
|
||||
assert.deepEqual(narrowed.operations, ["assignment.change"]);
|
||||
assert.deepEqual(narrowed.readPaths, [ROOT]);
|
||||
});
|
||||
|
||||
// --- closed vocabulary ---------------------------------------------------------------------
|
||||
|
||||
test("vocabularies are closed and consistent with the fixture index", () => {
|
||||
assert.deepEqual([...RESULTS].sort(), ["allowed", "invalid", "refused", "unresolved"]);
|
||||
assert.deepEqual(SUPPORTED_OPERATIONS, ["work.read", "file.read", "file.change", "assignment.change"]);
|
||||
assert.equal(new Set(RULES).size, RULES.length);
|
||||
for (const [step, outcome] of Object.entries(PROPOSAL_STEPS)) {
|
||||
assert.ok(RULES.includes(step), `proposal step ${step} is a rule`);
|
||||
assert.ok(["refused", "unresolved"].includes(outcome));
|
||||
}
|
||||
assert.equal(PROPOSAL_STEPS["runtime-reconciliation-required"], "unresolved");
|
||||
const used = new Set();
|
||||
for (const c of INDEX.cases) {
|
||||
assert.ok(RESULTS.includes(c.expect.result), c.name);
|
||||
assert.ok(REASONS.includes(c.expect.reason), c.name);
|
||||
if (c.expect.rule !== null) { assert.ok(RULES.includes(c.expect.rule), `${c.name} rule ${c.expect.rule}`); used.add(c.expect.rule); }
|
||||
if (c.expect.proposalRule !== null) assert.ok(RULES.includes(c.expect.proposalRule), c.name);
|
||||
}
|
||||
// Every stage-2..5 rule of the closed vocabulary is exercised by at least one fixture,
|
||||
// except CLI/I-O rules, which cli.test.mjs covers with a synthetic sandbox.
|
||||
const cliOnly = new Set(["usage-invalid", "open-flags-unavailable", "input-open-failed", "input-not-regular", "input-size-changed"]);
|
||||
const unexercised = RULES.filter((r) => !used.has(r) && !cliOnly.has(r) && !(r in PROPOSAL_STEPS));
|
||||
assert.deepEqual(unexercised, [], "rules without a fixture");
|
||||
});
|
||||
|
||||
test("closed result fields equal the pinned charter §7 list; exit is process metadata, not a field", () => {
|
||||
const fromCharter = charterResultFields();
|
||||
assert.deepEqual(fromCharter, RESULT_KEYS, "test copy must equal the charter text");
|
||||
assert.deepEqual([...IMPL_RESULT_KEYS], fromCharter, "implementation export must equal the charter text");
|
||||
assert.ok(!fromCharter.includes("exit"));
|
||||
const r = buildResult({ exit: 0, reason: "allowed", rule: null, selection: null, operation: null, proposal: null, diagnostic: null });
|
||||
assert.deepEqual(Object.keys(r), fromCharter);
|
||||
assert.ok(!("exit" in r));
|
||||
assert.equal(exitFor(r), 0);
|
||||
assert.equal(exitFor({ result: "refused", reason: "not-authorized" }), 3);
|
||||
assert.equal(exitFor({ result: "unresolved", reason: "unknown-effects" }), 3);
|
||||
assert.equal(exitFor({ result: "invalid", reason: "invalid-request" }), 2);
|
||||
assert.equal(exitFor({ result: "invalid", reason: "missing-state" }), 2);
|
||||
assert.equal(exitFor({ result: "invalid", reason: "io-failure" }), 4);
|
||||
for (const [exit, reason] of [[3, "allowed"], [0, "not-authorized"], [2, "io-failure"], [4, "invalid-request"]]) {
|
||||
assert.throws(() => buildResult({ exit, reason, rule: null, selection: null, operation: null, proposal: null, diagnostic: null }), /inconsistent/, `${exit}/${reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("buildResult: fixed key order, derived result, closed shape", () => {
|
||||
const r = buildResult({ exit: 0, reason: "allowed", rule: null, selection: null, operation: null, proposal: null, diagnostic: null });
|
||||
assert.deepEqual(Object.keys(r), RESULT_KEYS);
|
||||
assert.equal(r.disclaimer, DISCLAIMER);
|
||||
assert.equal(r.preview, PREVIEW);
|
||||
assert.equal(r.bundleVersion, 1);
|
||||
assert.equal(r.authentication, "not-modelled");
|
||||
assert.equal(r.declarations, "unverified-simulation");
|
||||
assert.equal(r.result, "allowed");
|
||||
assert.equal(buildResult({ exit: 3, reason: "not-authorized", rule: "operation-not-permitted", selection: null, operation: null, proposal: null, diagnostic: null }).result, "refused");
|
||||
assert.equal(buildResult({ exit: 3, reason: "unknown-effects", rule: "runtime-reconciliation-required", selection: null, operation: null, proposal: null, diagnostic: null }).result, "unresolved");
|
||||
assert.equal(buildResult({ exit: 2, reason: "invalid-request", rule: "usage-invalid", selection: null, operation: null, proposal: null, diagnostic: null }).result, "invalid");
|
||||
assert.equal(buildResult({ exit: 4, reason: "io-failure", rule: "input-open-failed", selection: null, operation: null, proposal: null, diagnostic: { byteOffset: null, inputPath: "x" } }).result, "invalid");
|
||||
const d = buildResult({ exit: 2, reason: "invalid-request", rule: "input-parse-failed", selection: null, operation: null, proposal: null, diagnostic: { byteOffset: 7 } });
|
||||
assert.deepEqual(d.diagnostic, { byteOffset: 7, inputPath: null });
|
||||
assert.throws(() => buildResult({ exit: 3, reason: "not-authorized", rule: "made-up", selection: null, operation: null, proposal: null, diagnostic: null }), /unknown rule/);
|
||||
assert.throws(() => buildResult({ exit: 3, reason: "made-up", rule: null, selection: null, operation: null, proposal: null, diagnostic: null }), /unknown reason/);
|
||||
});
|
||||
|
||||
test("Refusal rejects unknown rules and reasons at construction", () => {
|
||||
assert.throws(() => new Refusal(3, "not-authorized", "no-such-rule", null), /unknown rule/);
|
||||
assert.throws(() => new Refusal(3, "no-such-reason", "operation-not-permitted", null), /unknown reason/);
|
||||
const r = new Refusal(3, "not-authorized", "operation-not-permitted", "bundle.operation");
|
||||
assert.equal(r.detail, "bundle.operation");
|
||||
assert.equal(r.step, null);
|
||||
});
|
||||
|
||||
// --- record shape ----------------------------------------------------------------------------
|
||||
|
||||
test("validateRecordShape refuses unsupported kinds at the kind gate without judging payloads", () => {
|
||||
const base = loadBundle("demo-read-w1");
|
||||
const project = base.records.find((r) => r.kind === "project");
|
||||
assert.equal(validateRecordShape(project).verdict, "valid");
|
||||
const alien = { ...project, kind: "session", payload: 42 };
|
||||
const v = validateRecordShape(alien);
|
||||
assert.equal(v.verdict, "unsupported-kind");
|
||||
assert.equal(v.rule, "record-kind-unsupported");
|
||||
assert.equal(validateRecordShape({ ...project, kind: "nonsense" }).verdict, "invalid");
|
||||
assert.equal(validateRecordShape(null).verdict, "invalid");
|
||||
});
|
||||
|
||||
// --- evaluate over fixtures ---------------------------------------------------------------------
|
||||
|
||||
test("§10.4 precedence: cycle rules fire before parent-scope and predecessor-continuity refusals", () => {
|
||||
assert.equal(evaluate(loadBundle("cycle-mission-self-parent")).result.rule, "mission-parent-cycle");
|
||||
assert.equal(evaluate(loadBundle("cycle-supersedes-self-rev2")).result.rule, "supersedes-cycle");
|
||||
});
|
||||
|
||||
test("evaluate over every bundle fixture: closed result, no locator leakage, deterministic", () => {
|
||||
const names = readdirSync(join(FIXTURES, "bundles")).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
||||
assert.equal(names.length, INDEX.cases.filter((c) => c.raw === false).length);
|
||||
for (const name of names) {
|
||||
const expect = INDEX.cases.find((c) => c.name === name).expect;
|
||||
const bundle = loadBundle(name);
|
||||
const { result, exit, detail } = evaluate(bundle);
|
||||
assert.deepEqual(Object.keys(result), RESULT_KEYS, name);
|
||||
assert.ok(!("exit" in result), name);
|
||||
assert.equal(exit, expect.exit, name);
|
||||
assert.equal(exitFor(result), expect.exit, name);
|
||||
assert.equal(result.result, expect.result, name);
|
||||
assert.equal(result.reason, expect.reason, name);
|
||||
assert.equal(result.rule, expect.rule, name);
|
||||
assert.equal(result.proposal === null ? null : result.proposal.rule, expect.proposalRule, name);
|
||||
assert.equal(result.diagnostic, null, name);
|
||||
assert.ok(!("detail" in result), name);
|
||||
if (detail !== null) assert.equal(typeof detail, "string", name);
|
||||
assert.deepEqual(unknownKeys(result), [], `${name}: keys outside the closed result vocabulary`);
|
||||
assert.deepEqual(inspect(loadBundle(name)), result, `${name}: nondeterministic`);
|
||||
if (result.proposal !== null) {
|
||||
assert.equal(result.proposal.result, PROPOSAL_STEPS[result.proposal.rule], name);
|
||||
assert.equal(result.result, result.proposal.result, name);
|
||||
}
|
||||
if (exit === 0) {
|
||||
assert.equal(result.rule, null, name);
|
||||
assert.notEqual(result.selection, null, name);
|
||||
assert.notEqual(result.operation, null, name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("evaluate never mutates its input", () => {
|
||||
const bundle = loadBundle("demo-change-w1");
|
||||
const before = JSON.stringify(bundle);
|
||||
evaluate(bundle);
|
||||
assert.equal(JSON.stringify(bundle), before);
|
||||
});
|
||||
|
||||
// --- static import boundary (bounded evidence, not a sandbox) ------------------------------------
|
||||
|
||||
function importsOf(source) {
|
||||
return [...source.matchAll(/^import\s[^;]*?from\s+"([^"]+)";?$/gm)].map((m) => m[1]);
|
||||
}
|
||||
|
||||
function codeOnly(source) {
|
||||
// strip block and line comments before token scanning
|
||||
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1");
|
||||
}
|
||||
|
||||
const FORBIDDEN_PURE = /\b(process|Date|fetch|require|globalThis|setTimeout|setInterval|queueMicrotask|Intl|performance|Worker|Atomics|SharedArrayBuffer|WebAssembly)\b|Math\.random|import\s*\(/;
|
||||
|
||||
test("pure modules import only node:crypto and each other, and contain no ambient-capability tokens", () => {
|
||||
const allowed = new Set(["node:crypto", "./canonical.mjs", "./strict-json.mjs"]);
|
||||
for (const file of ["strict-json.mjs", "canonical.mjs", "resolve.mjs"]) {
|
||||
const source = readFileSync(join(HERE, file), "utf8");
|
||||
for (const spec of importsOf(source)) assert.ok(allowed.has(spec), `${file} imports ${spec}`);
|
||||
const code = codeOnly(source);
|
||||
const hit = FORBIDDEN_PURE.exec(code);
|
||||
assert.equal(hit, null, `${file} contains ${hit && hit[0]}`);
|
||||
assert.ok(!/\bnode:(fs|child_process|net|http|https|os|dns|tls|worker_threads|vm|readline|cluster|dgram)\b/.test(code), file);
|
||||
}
|
||||
});
|
||||
|
||||
test("CLI imports only node:fs/url/path and local modules; no process spawning or network modules", () => {
|
||||
const source = readFileSync(join(HERE, "..", "foundation-inspect.mjs"), "utf8");
|
||||
const allowed = new Set(["node:fs", "node:url", "node:path", "./foundation/strict-json.mjs", "./foundation/resolve.mjs"]);
|
||||
const specs = importsOf(source);
|
||||
assert.ok(specs.length >= 4);
|
||||
for (const spec of specs) assert.ok(allowed.has(spec), `CLI imports ${spec}`);
|
||||
const code = codeOnly(source);
|
||||
assert.ok(!/\b(child_process|node:net|node:http|node:https|node:os|node:dns|node:tls|worker_threads|node:vm|fetch|require|Date|globalThis|setTimeout|readdirSync|readFileSync|writeFileSync|homedir|tmpdir)\b/.test(code));
|
||||
assert.ok(!/process\.env|process\.cwd|import\s*\(|process\.chdir|process\.stdin/.test(code));
|
||||
// Only stdout write, exitCode assignment and argv read are ambient uses of `process`.
|
||||
const uses = [...code.matchAll(/process\.(\w+)/g)].map((m) => m[1]);
|
||||
assert.deepEqual([...new Set(uses)].sort(), ["argv", "exitCode", "stdout"]);
|
||||
});
|
||||
|
||||
// --- schema column vs strict profile (addendum FI-C2-1; verdict FI-FILBERT-5) -------------
|
||||
const REVIEWS = join(HERE, "..", "..", "docs", "plans", "reviews");
|
||||
const ADDENDUM = join(REVIEWS, "2026-09-06_foundation-inspector-pattern-profile-addendum.md");
|
||||
const ADDENDUM_SHA256 = "afe2980be2f91e701dae5af3018831ac5c300474f52bcc06e740ce5b5cc68ca5";
|
||||
const VERDICT = join(REVIEWS, "2026-09-06_foundation-inspector-pattern-profile-verdict.md");
|
||||
const VERDICT_SHA256 = "03c979b77cc6b03b6685ba51ed1ce24c3f1d7b274ce8f772263a103670050da7";
|
||||
|
||||
test("the unchanged charter, the exact addendum and its verdict are bound; the profile rule is in the closed vocabulary", () => {
|
||||
const sha = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
||||
assert.equal(sha(CHARTER), CHARTER_SHA256, "charter candidate 3 unchanged");
|
||||
assert.equal(sha(ADDENDUM), ADDENDUM_SHA256, "addendum FI-C2-1 must be the exact reviewed text; rebind on amendment");
|
||||
assert.equal(sha(VERDICT), VERDICT_SHA256, "verdict FI-FILBERT-5 must be the exact text; rebind on amendment");
|
||||
assert.ok(readFileSync(ADDENDUM, "utf8").includes("profile-pattern-mismatch"), "addendum names the fixed profile rule");
|
||||
assert.ok(RULES.includes("profile-pattern-mismatch"));
|
||||
assert.ok(RULES.indexOf("profile-pattern-mismatch") < RULES.indexOf("duplicate-record-identity"), "stage-2 rule, before identity");
|
||||
});
|
||||
|
||||
const SAMPLES = { id: "task-w1", runtimeId: "00000000-0000-4000-8000-000000000001", digest: `sha256:${"0".repeat(64)}` };
|
||||
const NEWLINE_MATRIX = [ // label, mutation, pinned-schema verdict, strict-profile verdict (FI-FILBERT-5 table)
|
||||
["valid", (v) => v, true, true],
|
||||
["one-final-lf", (v) => `${v}\n`, true, false],
|
||||
["two-final-lf", (v) => `${v}\n\n`, false, false],
|
||||
["crlf", (v) => `${v}\r\n`, false, false],
|
||||
["cr", (v) => `${v}\r`, false, false],
|
||||
["u2028", (v) => `${v}\u2028`, false, false],
|
||||
["u2029", (v) => `${v}\u2029`, false, false],
|
||||
["interior-lf", (v) => `${v.slice(0, 3)}\n${v.slice(3)}`, false, false],
|
||||
];
|
||||
|
||||
test("schema column reproduces the pinned checker's end-anchor semantics; the profile column is strict", () => {
|
||||
assert.deepEqual(Object.keys(PINNED_PATTERNS), ["id", "runtimeId", "digest"]);
|
||||
for (const [family, sample] of Object.entries(SAMPLES)) {
|
||||
const re = PINNED_PATTERNS[family];
|
||||
assert.ok(re.source.endsWith("$") && !re.flags.includes("m"), `${family} is end-anchored without multiline`);
|
||||
for (const [label, mutate, schema, profile] of NEWLINE_MATRIX) {
|
||||
const v = mutate(sample);
|
||||
assert.equal(pinnedPatternMatches(re, v), schema, `${family}/${label} schema column`);
|
||||
assert.equal(strictPatternMatches(re, v), profile, `${family}/${label} profile column`);
|
||||
assert.ok(schema || !profile, "profile-valid implies schema-valid");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("validateRecordShape reports schema and profile verdicts independently, in validation order, without altering the value", () => {
|
||||
const base = loadBundle("demo-read-w1").records.find((r) => r.kind === "task" && r.id === "task-w1");
|
||||
const clone = () => JSON.parse(JSON.stringify(base));
|
||||
assert.deepEqual(validateRecordShape(clone()), { verdict: "valid", profileViolations: [] });
|
||||
const one = clone(); one.id = `${one.id}\n`;
|
||||
const v1 = validateRecordShape(one);
|
||||
assert.deepEqual(v1, { verdict: "valid", profileViolations: ["record.id"] });
|
||||
assert.equal(one.id, `${base.id}\n`, "no trimming or normalization");
|
||||
const two = clone(); two.id = `${two.id}\n\n`;
|
||||
assert.deepEqual(validateRecordShape(two), { verdict: "invalid", rule: "shape-pattern-mismatch", path: "record.id" });
|
||||
const many = clone();
|
||||
many.id = `${many.id}\n`; many.scope.projectId = `${many.scope.projectId}\n`; many.authorizationRef = `${many.authorizationRef}\n`;
|
||||
many.payload.dependencies[0].id = `${many.payload.dependencies[0].id}\n`;
|
||||
assert.deepEqual(validateRecordShape(many).profileViolations,
|
||||
["record.id", "record.scope.projectId", "record.authorizationRef", "record.payload.dependencies[0].id"]);
|
||||
const text = clone(); text.payload.purpose = "line one\nline two\r\n";
|
||||
assert.deepEqual(validateRecordShape(text), { verdict: "valid", profileViolations: [] }, "free-form text is not a typed grammar");
|
||||
const time = clone(); time.createdAt = `${time.createdAt}\n`;
|
||||
assert.deepEqual(validateRecordShape(time), { verdict: "invalid", rule: "shape-pattern-mismatch", path: "record.createdAt" });
|
||||
});
|
||||
|
||||
test("profile refusal happens before identity lookup and is distinguishable from a missing reference", () => {
|
||||
const missing = evaluate(loadBundle("ref-missing-dependency"));
|
||||
assert.equal(missing.result.rule, "record-reference-missing");
|
||||
const lf = loadBundle("profile-record-dependency-ref-id-dangling-one-final-lf");
|
||||
const r = evaluate(lf);
|
||||
assert.equal(r.exit, 2);
|
||||
assert.equal(r.result.result, "invalid");
|
||||
assert.equal(r.result.reason, "invalid-request");
|
||||
assert.equal(r.result.rule, "profile-pattern-mismatch");
|
||||
assert.equal(r.result.selection, null, "safe pre-admission output");
|
||||
assert.equal(r.result.operation, null);
|
||||
assert.equal(r.result.proposal, null);
|
||||
assert.equal(r.result.diagnostic, null);
|
||||
assert.deepEqual(Object.keys(r.result), [...IMPL_RESULT_KEYS], "closed fields unchanged");
|
||||
assert.ok(/^bundle\.records\[\d+\]\.payload\.dependencies\[0\]\.id$/.test(r.detail), "internal locator is the path, not the value");
|
||||
assert.ok(!JSON.stringify(r.result).includes("task-nope"), "the value never enters the result");
|
||||
// Every profile fixture: selection/operation withheld, no typed value with a newline echoed.
|
||||
for (const c of INDEX.cases.filter((x) => x.expect.rule === "profile-pattern-mismatch")) {
|
||||
const out = evaluate(loadBundle(c.name));
|
||||
assert.equal(out.exit, 2, c.name);
|
||||
assert.equal(out.result.selection, null, c.name);
|
||||
assert.equal(out.result.operation, null, c.name);
|
||||
assert.ok(!JSON.stringify(out.result).includes("\\n"), `${c.name}: newline-bearing value echoed`);
|
||||
}
|
||||
assert.ok(INDEX.cases.filter((x) => x.expect.rule === "profile-pattern-mismatch").length >= 30);
|
||||
});
|
||||
|
||||
// --- FI-FILBERT-6 corrections (F1–F5): witness recipes as regression tests -------------------
|
||||
|
||||
function firstFailure(bundle) {
|
||||
const { result, exit } = evaluate(bundle);
|
||||
return { exit, result: result.result, reason: result.reason, rule: result.rule, step: result.proposal === null ? null : result.proposal.rule };
|
||||
}
|
||||
|
||||
test("F1: consulted registry references resolve by exact four-field declaration at the admission stage", () => {
|
||||
assert.ok(RULES.includes("registry-declaration-missing"));
|
||||
const missing = { exit: 3, result: "refused", reason: "missing-state", rule: "registry-declaration-missing", step: null };
|
||||
for (const name of [
|
||||
"adm-registry-declaration-missing-agent-refs", "adm-registry-declaration-missing-settings", "adm-registry-declaration-missing-soul",
|
||||
"adm-registry-declaration-digest-mismatch", "adm-registry-declaration-revision-mismatch",
|
||||
"adm-registry-declaration-missing-instruction", "adm-registry-declaration-missing-skill",
|
||||
"adm-registry-declaration-missing-endpoint-execution", "adm-registry-declaration-missing-endpoint-task",
|
||||
]) assert.deepEqual(firstFailure(loadBundle(name)), missing, name);
|
||||
// never the ordinary-record structural rule, never an allowed preview
|
||||
const b = loadBundle("demo-read-w1");
|
||||
b.registries = b.registries.filter((e) => e.registry !== "harness");
|
||||
const r = firstFailure(b);
|
||||
assert.notEqual(r.rule, "record-reference-missing");
|
||||
assert.equal(r.exit, 3);
|
||||
// positive controls: declared endpoint, unconsulted record's dangling reference
|
||||
assert.equal(firstFailure(loadBundle("adm-registry-declaration-endpoint-declared")).exit, 0);
|
||||
assert.equal(firstFailure(loadBundle("adm-registry-declaration-unselected-agent-ignored")).exit, 0);
|
||||
});
|
||||
|
||||
test("F1: consulted dependency and mission authorizations must be declared", () => {
|
||||
const undeclared = { exit: 3, result: "refused", reason: "missing-state", rule: "authorization-undeclared", step: null };
|
||||
assert.deepEqual(firstFailure(loadBundle("adm-dependency-authorization-undeclared")), undeclared);
|
||||
assert.deepEqual(firstFailure(loadBundle("adm-mission-authorization-undeclared")), undeclared);
|
||||
assert.equal(firstFailure(loadBundle("adm-unconsulted-authorization-ignored")).exit, 0);
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-subject-task-authorization-undeclared")), { ...undeclared, step: "intent-not-current" });
|
||||
});
|
||||
|
||||
test("F2: a bound task is consulted work; work.read is required even without mission/dependency references", () => {
|
||||
const notReadable = { exit: 3, result: "refused", reason: "not-authorized", rule: "consulted-work-not-readable", step: null };
|
||||
assert.deepEqual(firstFailure(loadBundle("adm-assigned-task-without-work-read")), notReadable);
|
||||
assert.deepEqual(firstFailure(loadBundle("adm-assigned-task-without-work-read-read-op")), notReadable);
|
||||
assert.equal(firstFailure(loadBundle("assigned-task-only-with-work-read")).exit, 0);
|
||||
// genuinely taskless reads keep skipping the assignment layer
|
||||
assert.equal(firstFailure(loadBundle("taskless-work-read")).exit, 0);
|
||||
assert.equal(firstFailure(loadBundle("taskless-file-read")).exit, 0);
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-task-only-contexts-without-work-read")), { ...notReadable, step: "requester-lacks-original-scope-authority" });
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-task-only-target-context-without-work-read")), { ...notReadable, step: "requester-lacks-target-scope-authority" });
|
||||
assert.equal(firstFailure(loadBundle("prop-task-only-contexts-with-work-read")).rule, "runtime-reconciliation-required");
|
||||
});
|
||||
|
||||
test("F3: the issuer ceiling narrows both requester calculations and work access is rechecked", () => {
|
||||
const expect = { exit: 3, result: "refused", reason: "not-authorized", rule: "consulted-work-not-readable", step: "requester-lacks-original-scope-authority" };
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-issuer-denies-work-read")), expect);
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-issuer-denies-work-read-task-only")), expect);
|
||||
assert.equal(firstFailure(loadBundle("prop-issuer-grants-work-read")).rule, "runtime-reconciliation-required");
|
||||
// the narrowing is the same in both scopes, so the original scope is always the first failure;
|
||||
// the layer algebra itself is scope-independent
|
||||
const issuer = layerFromRestrictions({ operations: ["assignment.change"], readPaths: [], writePaths: [], network: "none", endpointRefs: [] });
|
||||
const ctx = layerFromRestrictions({ operations: ["work.read", "assignment.change"], readPaths: [], writePaths: [], network: "none", endpointRefs: [] });
|
||||
assert.deepEqual(intersectLayers(ctx, issuer).operations, ["assignment.change"]);
|
||||
});
|
||||
|
||||
test("F4: cycle graphs are revision-exact; contiguous history with reversed edges is not a cycle", () => {
|
||||
assert.equal(firstFailure(loadBundle("cycle-acyclic-versioned-history")).exit, 0);
|
||||
assert.equal(firstFailure(loadBundle("cycle-acyclic-versioned-history-file-change")).exit, 0);
|
||||
assert.equal(firstFailure(loadBundle("cycle-dependency-cross-revision")).rule, "dependency-cycle");
|
||||
assert.equal(firstFailure(loadBundle("cycle-mission-parent-old-revision-only")).rule, "mission-parent-cycle");
|
||||
assert.equal(firstFailure(loadBundle("cycle-mission-parent-previous-revision-not-a-cycle")).rule, "mission-parent-scope-mismatch");
|
||||
// detector-level witness: the exact graph of the history is acyclic, the identity-merged one is not
|
||||
const exact = new Map([["T#1", ["D#1"]], ["D#1", []], ["T#2", []], ["D#2", ["T#2"]]]);
|
||||
assert.equal(findCycle(exact), null);
|
||||
const merged = new Map([["T", ["D"]], ["D", ["T"]]]);
|
||||
assert.equal(findCycle(merged), "D");
|
||||
});
|
||||
|
||||
test("F5: the first shape failure is independent of record input order, including malformed records", () => {
|
||||
const forward = loadBundle("shape-order-forward");
|
||||
const reference = firstFailure(forward);
|
||||
assert.equal(reference.rule, "shape-enum-mismatch");
|
||||
const permutations = [
|
||||
(r) => [...r].reverse(),
|
||||
(r) => [...r.slice(7), ...r.slice(0, 7)],
|
||||
(r) => [...r].sort((a, b) => (a.id < b.id ? 1 : -1)),
|
||||
(r) => r.filter((_, i) => i % 2 === 1).concat(r.filter((_, i) => i % 2 === 0)),
|
||||
];
|
||||
for (const [i, perm] of permutations.entries()) {
|
||||
const b = loadBundle("shape-order-forward");
|
||||
b.records = perm(b.records);
|
||||
assert.deepEqual(firstFailure(b), reference, `permutation ${i}`);
|
||||
}
|
||||
assert.deepEqual(firstFailure(loadBundle("shape-order-reversed")), reference);
|
||||
// malformed records (no readable identity) are validated after every identified record, wherever they sit
|
||||
const withMalformed = loadBundle("shape-order-malformed-record-sorts-last");
|
||||
assert.equal(firstFailure(withMalformed).rule, "shape-enum-mismatch");
|
||||
const moved = loadBundle("shape-order-malformed-record-sorts-last");
|
||||
moved.records = [...moved.records.slice(1), moved.records[0]];
|
||||
assert.deepEqual(firstFailure(moved), firstFailure(withMalformed));
|
||||
assert.equal(firstFailure(loadBundle("shape-order-only-malformed-record")).rule, "shape-missing-field");
|
||||
// two unidentified records: content decides, not position
|
||||
const twoBad = loadBundle("demo-read-w1");
|
||||
const a = { schemaVersion: 1 };
|
||||
const c = { schemaVersion: "1" };
|
||||
twoBad.records = [a, c, ...twoBad.records];
|
||||
const first = firstFailure(twoBad);
|
||||
twoBad.records = [c, ...twoBad.records.slice(2), a];
|
||||
assert.deepEqual(firstFailure(twoBad), first);
|
||||
});
|
||||
|
||||
// --- FI-ROCKO-7: stable traversal of the declared inventories (§10.4) -------------------------
|
||||
|
||||
/** First failure plus its internal locator with the input index replaced by the entry's typed key. */
|
||||
function firstLocated(bundle, family) {
|
||||
const { result, exit, detail } = evaluate(bundle);
|
||||
const m = typeof detail === "string" ? detail.match(new RegExp(`^bundle\\.${family}\\[(\\d+)\\]`)) : null;
|
||||
const entry = m === null ? null : bundle[family][Number(m[1])];
|
||||
return { exit, result: result.result, reason: result.reason, rule: result.rule, step: result.proposal === null ? null : result.proposal.rule,
|
||||
tail: m === null ? detail : detail.slice(m[0].length), key: entry === null ? null : ORDER_KEYS[family](entry) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Permutations of index array `arr`: every permutation when n ≤ 4, otherwise a fixed
|
||||
* deterministic family (identity, reverse, every rotation, odd/even interleave, a
|
||||
* descending sort and twelve seeded Fisher–Yates shuffles). Includes the identity.
|
||||
*/
|
||||
function permutations(arr) {
|
||||
if (arr.length <= 4) {
|
||||
if (arr.length <= 1) return [arr];
|
||||
return arr.flatMap((x, i) => permutations([...arr.slice(0, i), ...arr.slice(i + 1)]).map((rest) => [x, ...rest]));
|
||||
}
|
||||
const out = [arr, [...arr].reverse(), [...arr].sort((x, y) => y - x), arr.filter((_, i) => i % 2 === 1).concat(arr.filter((_, i) => i % 2 === 0))];
|
||||
for (let k = 1; k < arr.length; k += 1) out.push([...arr.slice(k), ...arr.slice(0, k)]);
|
||||
let seed = 0x9e3779b9;
|
||||
const next = () => { seed = (Math.imul(seed ^ (seed >>> 15), 0x2c1b3c6d) >>> 0); seed = (seed ^ (seed >>> 12)) >>> 0; return seed / 0x100000000; };
|
||||
for (let s = 0; s < 12; s += 1) {
|
||||
const p = [...arr];
|
||||
for (let i = p.length - 1; i > 0; i -= 1) { const j = Math.floor(next() * (i + 1)); [p[i], p[j]] = [p[j], p[i]]; }
|
||||
out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Assert that every permutation of bundle[family] yields the same located first failure; returns it. */
|
||||
function assertPermutationStable(name, family, expectRule, expectKey) {
|
||||
const reference = firstLocated(loadBundle(name), family);
|
||||
assert.equal(reference.rule, expectRule, name);
|
||||
if (expectKey !== undefined) assert.deepEqual(reference.key, expectKey, `${name}: first failure locator`);
|
||||
const base = loadBundle(name);
|
||||
const perms = permutations(base[family].map((_, i) => i));
|
||||
assert.ok(perms.length >= 2, `${name}: ${family} has a permutable inventory`);
|
||||
for (const perm of perms) {
|
||||
const b = loadBundle(name);
|
||||
b[family] = perm.map((i) => base[family][i]);
|
||||
assert.deepEqual(firstLocated(b, family), reference, `${name}: permutation ${JSON.stringify(perm)}`);
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
test("stableOrder: typed keys, null components after values, unkeyed items last, ordering-form tie-break, then index", () => {
|
||||
const items = [
|
||||
{ registry: "scope-role", id: "b", revision: 2, digest: "x" }, // 0
|
||||
{ registry: "scope-role", id: "b", revision: 1, digest: "x" }, // 1
|
||||
{ registry: "agent-policy", id: "z", revision: 1, digest: "x" }, // 2
|
||||
"not-an-entry", // 3 (unkeyed)
|
||||
{ registry: "scope-role", id: "b", revision: null, digest: "x" }, // 4 (null revision after integers)
|
||||
{ registry: 7 }, // 5 (unkeyed)
|
||||
{ registry: "scope-role", id: "b", revision: 1, digest: "x" }, // 6 (duplicate of 1: canonical tie, index decides)
|
||||
{ registry: "scope-role", id: "b", revision: 1, digest: "a" }, // 7 (digest "a" before "x")
|
||||
];
|
||||
assert.deepEqual(stableOrder(items, ORDER_KEYS.registries), [2, 7, 1, 6, 0, 4, 3, 5]);
|
||||
// unkeyed items order by their ordering form: "not-an-entry" (string) vs {"registry":7} (object); reversing the input does not change it
|
||||
const rev = [...items].reverse();
|
||||
const order = stableOrder(rev, ORDER_KEYS.registries).map((i) => rev[i]);
|
||||
assert.deepEqual(order, stableOrder(items, ORDER_KEYS.registries).map((i) => items[i]));
|
||||
// records: kind/id/scope/revision, unreadable identity last
|
||||
const recs = [{ kind: "task", id: "t", scope: { kind: "system" }, revision: 2 }, { schemaVersion: 1 }, { kind: "task", id: "t", scope: { kind: "system" }, revision: 1 }, { kind: "agent-definition", id: "a", scope: { kind: "project", projectId: "p" }, revision: 1 }];
|
||||
assert.deepEqual(stableOrder(recs, ORDER_KEYS.records), [3, 2, 0, 1]);
|
||||
assert.deepEqual(stableOrder(["b", "a", 7, "a"], ORDER_KEYS.authorizations), [1, 3, 0, 2]);
|
||||
assert.deepEqual(stableOrder([{ runId: "r", artifactId: "b", digest: "d" }, { runId: "r", artifactId: "a" }, { runId: 1 }], ORDER_KEYS.artifacts), [1, 0, 2]);
|
||||
assert.deepEqual(stableOrder([{ decisionRef: { kind: "decision", id: "b", scope: { kind: "system" }, revision: 1 } }, { decisionRef: null }, { decisionRef: { kind: "decision", id: "a", scope: { kind: "system" }, revision: 1 } }], ORDER_KEYS.delegationInputs), [2, 0, 1]);
|
||||
// semantic sequence arrays are never reordered: the helper is only applied to inventories
|
||||
const b = loadBundle("demo-read-w1");
|
||||
const before = JSON.stringify(b);
|
||||
evaluate(b);
|
||||
assert.equal(JSON.stringify(b), before, "evaluation does not mutate or reorder the input bundle");
|
||||
});
|
||||
|
||||
test("ordering: registries — differing shape errors, permutation-stable first rule follows the typed key", () => {
|
||||
const apFull = ["agent-policy", "ap-full", 1, null];
|
||||
assertPermutationStable("shape-order-registries-forward", "registries", "shape-missing-field", apFull);
|
||||
assertPermutationStable("shape-order-registries-reversed", "registries", "shape-missing-field", apFull);
|
||||
// swapping which entry carries which error changes the first rule, not the winning key
|
||||
const swapped = assertPermutationStable("shape-order-registries-swapped-forward", "registries", "shape-unknown-field");
|
||||
assert.deepEqual(swapped.key.slice(0, 2), ["agent-policy", "ap-full"]);
|
||||
assertPermutationStable("shape-order-registries-swapped-reversed", "registries", "shape-unknown-field");
|
||||
// an entry without a readable registry/id is validated after every keyed entry
|
||||
const malformed = assertPermutationStable("shape-order-registries-malformed-sorts-last", "registries", "shape-enum-mismatch");
|
||||
assert.deepEqual(malformed.key.slice(0, 2), ["no-such-registry", "role-writer"]);
|
||||
assert.equal(malformed.tail, ".registry");
|
||||
const only = assertPermutationStable("shape-order-registries-only-malformed", "registries", "shape-type-mismatch");
|
||||
assert.equal(only.key, null);
|
||||
});
|
||||
|
||||
test("ordering: registries — profile then digest stages follow the same key; stage precedence is fixed", () => {
|
||||
const apReader = ["agent-policy", "ap-reader\n", 1];
|
||||
const p = assertPermutationStable("profile-order-registries-forward", "registries", "profile-pattern-mismatch");
|
||||
assert.deepEqual(p.key.slice(0, 3), apReader);
|
||||
assert.equal(p.tail, ".id");
|
||||
assertPermutationStable("profile-order-registries-reversed", "registries", "profile-pattern-mismatch");
|
||||
// shape (later key) precedes profile (earlier key)
|
||||
const sp = assertPermutationStable("profile-order-shape-precedes-profile-registries", "registries", "shape-unknown-field");
|
||||
assert.deepEqual(sp.key.slice(0, 2), ["project-policy", "pp-p2"]);
|
||||
// digest stage: unsupported content on the lower key precedes a mismatch on the higher key, and vice versa
|
||||
const d = assertPermutationStable("shape-order-registry-digest-forward", "registries", "mock-content-unsupported");
|
||||
assert.deepEqual(d.key.slice(0, 2), ["agent-policy", "ap-full"]);
|
||||
assert.equal(d.reason, "unsupported-capability");
|
||||
assertPermutationStable("shape-order-registry-digest-reversed", "registries", "mock-content-unsupported");
|
||||
const ds = assertPermutationStable("shape-order-registry-digest-swapped-forward", "registries", "registry-digest-mismatch");
|
||||
assert.deepEqual(ds.key.slice(0, 2), ["agent-policy", "ap-full"]);
|
||||
assert.equal(ds.tail, ".digest");
|
||||
assertPermutationStable("shape-order-registry-digest-swapped-reversed", "registries", "registry-digest-mismatch");
|
||||
// profile (later key) precedes digest (earlier key)
|
||||
const pd = assertPermutationStable("shape-order-profile-precedes-digest-registries", "registries", "profile-pattern-mismatch");
|
||||
assert.deepEqual(pd.key.slice(0, 2), ["project-policy", "pp-p2\n"]);
|
||||
// two competing digest mismatches: the lower key is reported in every order
|
||||
const two = assertPermutationStable("shape-order-two-digest-mismatches-reversed", "registries", "registry-digest-mismatch");
|
||||
assert.deepEqual(two.key.slice(0, 2), ["scope-role", "role-reader"]);
|
||||
});
|
||||
|
||||
test("ordering: artifacts — differing shape errors and profile violations are permutation-stable", () => {
|
||||
const art1 = assertPermutationStable("shape-order-artifacts-forward", "artifacts", "shape-pattern-mismatch");
|
||||
assert.deepEqual(art1.key.slice(0, 2), ["run-1", "art-1"]);
|
||||
assert.equal(art1.tail, ".digest");
|
||||
assertPermutationStable("shape-order-artifacts-reversed", "artifacts", "shape-pattern-mismatch");
|
||||
const sw = assertPermutationStable("shape-order-artifacts-swapped-forward", "artifacts", "shape-unknown-field");
|
||||
assert.deepEqual(sw.key.slice(0, 2), ["run-1", "art-1"]);
|
||||
assertPermutationStable("shape-order-artifacts-swapped-reversed", "artifacts", "shape-unknown-field");
|
||||
const mal = assertPermutationStable("shape-order-artifacts-malformed-sorts-last", "artifacts", "shape-pattern-mismatch");
|
||||
assert.deepEqual(mal.key.slice(0, 2), ["run-1", "art-2"]);
|
||||
const prof = assertPermutationStable("profile-order-artifacts-reversed", "artifacts", "profile-pattern-mismatch");
|
||||
assert.deepEqual(prof.key.slice(0, 2), ["run-1", "art-2\n"]);
|
||||
assert.equal(prof.tail, ".artifactId");
|
||||
});
|
||||
|
||||
test("ordering: authorizations — the lower runtime id is the locator in every order; shape precedes profile", () => {
|
||||
const low = assertPermutationStable("shape-order-authorizations-forward", "authorizations", "shape-pattern-mismatch");
|
||||
assert.deepEqual(low.key, ["00000000-0000-1000-8000-000000000002"]);
|
||||
assertPermutationStable("shape-order-authorizations-reversed", "authorizations", "shape-pattern-mismatch");
|
||||
const mal = assertPermutationStable("shape-order-authorizations-malformed-sorts-last", "authorizations", "shape-pattern-mismatch");
|
||||
assert.deepEqual(mal.key, ["00000000-0000-4000-8000-000000000001-x"]);
|
||||
const prof = assertPermutationStable("profile-order-authorizations-reversed", "authorizations", "profile-pattern-mismatch");
|
||||
assert.deepEqual(prof.key, ["00000000-0000-4000-8000-000000000001\n"]);
|
||||
const sp = assertPermutationStable("profile-order-shape-precedes-profile-authorizations", "authorizations", "shape-pattern-mismatch");
|
||||
assert.deepEqual(sp.key, ["00000000-0000-4000-8000-000000000002-x"]);
|
||||
});
|
||||
|
||||
test("ordering: delegationInputs — shape, identity and structural stages traverse by decisionRef key", () => {
|
||||
const delegB = ["decision", "d-deleg-b", "workspace:p1/w1", 1];
|
||||
const f = assertPermutationStable("shape-order-delegation-inputs-forward", "delegationInputs", "shape-enum-mismatch", delegB);
|
||||
assert.equal(f.tail, ".mode");
|
||||
assertPermutationStable("shape-order-delegation-inputs-reversed", "delegationInputs", "shape-enum-mismatch", delegB);
|
||||
assertPermutationStable("shape-order-delegation-inputs-swapped-forward", "delegationInputs", "shape-missing-field", delegB);
|
||||
assertPermutationStable("shape-order-delegation-inputs-swapped-reversed", "delegationInputs", "shape-missing-field", delegB);
|
||||
assertPermutationStable("shape-order-delegation-inputs-malformed-sorts-last", "delegationInputs", "shape-enum-mismatch", delegB);
|
||||
// structural: both inputs reference a missing revision; the lower key is reported wherever it sits
|
||||
const s = assertPermutationStable("struct-order-delegation-inputs-forward", "delegationInputs", "record-reference-missing", [...delegB.slice(0, 3), 9]);
|
||||
assert.equal(s.reason, "missing-state");
|
||||
assert.equal(s.tail, ".decisionRef");
|
||||
assertPermutationStable("struct-order-delegation-inputs-reversed", "delegationInputs", "record-reference-missing", [...delegB.slice(0, 3), 9]);
|
||||
// identity: a duplicated input is found after a valid lower-key input in every order
|
||||
const dup = assertPermutationStable("dup-order-delegation-inputs-reversed", "delegationInputs", "duplicate-delegation-input");
|
||||
assert.deepEqual(dup.key.slice(0, 2), ["decision", "d-deleg-reg"]);
|
||||
});
|
||||
|
||||
test("ordering: family precedence and identity-stage order are fixed regardless of input positions", () => {
|
||||
assert.equal(firstFailure(loadBundle("shape-order-family-precedence-registries-before-artifacts")).rule, "shape-unknown-field");
|
||||
assert.equal(firstFailure(loadBundle("shape-order-family-precedence-artifacts-before-authorizations")).rule, "shape-pattern-mismatch");
|
||||
assert.equal(firstFailure(loadBundle("shape-order-family-precedence-authorizations-before-delegation-inputs")).rule, "shape-type-mismatch");
|
||||
assert.equal(firstFailure(loadBundle("dup-order-registries-precede-artifacts")).rule, "duplicate-registry-identity");
|
||||
// duplicate plain-registry identity with differing digests: the higher digest is the reported entry in every order
|
||||
const base = loadBundle("demo-read-w1");
|
||||
const harness = base.registries.find((e) => e.registry === "harness");
|
||||
base.registries.push({ ...harness, digest: "sha256:" + "f".repeat(64) });
|
||||
const ref = firstLocated(base, "registries");
|
||||
assert.equal(ref.rule, "duplicate-registry-identity");
|
||||
assert.equal(ref.key[3], "sha256:" + "f".repeat(64));
|
||||
const moved = { ...base, registries: [base.registries[base.registries.length - 1], ...base.registries.slice(0, -1)] };
|
||||
assert.deepEqual(firstLocated(moved, "registries"), ref);
|
||||
const lower = { ...base, registries: base.registries.map((e) => (e.digest === "sha256:" + "f".repeat(64) ? { ...e, digest: "sha256:" + "0".repeat(64) } : e)) };
|
||||
assert.equal(firstLocated(lower, "registries").key[3], harness.digest, "the original (now higher) digest is reported");
|
||||
// artifacts: duplicate run/artifact with differing digests, permutation-stable locator
|
||||
const art = loadBundle("dup-artifact-identity");
|
||||
const a = firstLocated(art, "artifacts");
|
||||
assert.equal(a.rule, "duplicate-artifact-identity");
|
||||
const artRev = { ...art, artifacts: [...art.artifacts].reverse() };
|
||||
assert.deepEqual(firstLocated(artRev, "artifacts"), a);
|
||||
});
|
||||
|
||||
test("R5-1: the ordering form is total and injective over the strict-JSON domain, distinct from the mock digest", () => {
|
||||
// every parser-producible value has a form; canonicalize() refuses most of these
|
||||
const values = [null, true, false, 0, -1, 9007199254740991, "", "a", "é", "É", "é", "\u{1F600}", "café", "cafe",
|
||||
" ", " ", "\\", "\"", [], [null], [1, 2], [2, 1], [[1], 2], [1, [2]], {}, { a: 1 }, { a: 2 }, { b: 1 }, { a: 1, b: 2 },
|
||||
{ a: { b: 1 } }, { a: [1] }, { "é": 1 }, { "é": "é" }, { extra: "é" }, { x: "é", y: null }, [{ extra: "é" }], ["é"]];
|
||||
const forms = values.map(orderingForm);
|
||||
for (const f of forms) assert.equal(typeof f, "string");
|
||||
assert.equal(new Set(forms).size, values.length, "distinct values have distinct forms");
|
||||
let refused = 0;
|
||||
for (const v of values) { try { canonicalize(v); } catch (e) { assert.ok(e instanceof CanonicalError); refused += 1; } }
|
||||
assert.ok(refused >= 12, "the mock digest domain is narrower than the ordering-form domain");
|
||||
// key order is by UTF-16 code unit and does not depend on insertion order; array order is semantic
|
||||
assert.equal(orderingForm({ b: 1, a: 2 }), orderingForm({ a: 2, b: 1 }));
|
||||
assert.equal(orderingForm({ b: 1, a: 2 }), '{"a":2,"b":1}');
|
||||
assert.equal(orderingForm({ "é": 1, e: 1, z: 1 }), '{"e":1,"z":1,"é":1}');
|
||||
assert.notEqual(orderingForm([1, 2]), orderingForm([2, 1]));
|
||||
assert.equal(orderingForm("é"), JSON.stringify("é"));
|
||||
assert.equal(orderingForm([{ extra: "é" }, "é"]), '[{"extra":"é"},"é"]');
|
||||
// no normalization or case folding; the form never throws for non-ASCII and is not a validity judgement
|
||||
assert.notEqual(orderingForm("é"), orderingForm("é"));
|
||||
assert.equal(orderingForm("é"), orderingForm("é"));
|
||||
assert.throws(() => orderingForm(undefined), /non-JSON/);
|
||||
assert.throws(() => orderingForm(() => 1), /non-JSON/);
|
||||
// stableOrder on two distinct unkeyed Unicode entries: string form before object form, in both input orders
|
||||
for (const family of ["records", "registries", "artifacts", "delegationInputs"]) {
|
||||
assert.deepEqual(stableOrder([{ extra: "é" }, "é"], ORDER_KEYS[family]), [1, 0], family);
|
||||
assert.deepEqual(stableOrder(["é", { extra: "é" }], ORDER_KEYS[family]), [0, 1], family);
|
||||
}
|
||||
// equal typed keys with distinct Unicode bodies: the form decides, never the index
|
||||
const a = { registry: "agent-policy", id: "ap", revision: 1, digest: "d", content: { name: "café", z: 1 } };
|
||||
const b = { registry: "agent-policy", id: "ap", revision: 1, digest: "d", content: { name: "café", a: 1 } };
|
||||
assert.deepEqual(stableOrder([a, b], ORDER_KEYS.registries), [1, 0]);
|
||||
assert.deepEqual(stableOrder([b, a], ORDER_KEYS.registries), [0, 1]);
|
||||
// only byte-identical entries fall back to the input index
|
||||
assert.deepEqual(stableOrder([a, structuredClone(a)], ORDER_KEYS.registries), [0, 1]);
|
||||
});
|
||||
|
||||
test("R5-1: unkeyed Unicode entries — the located first failure is permutation-stable in every family", () => {
|
||||
for (const [family, slug] of [["records", "records"], ["registries", "registries"], ["artifacts", "artifacts"], ["delegationInputs", "delegation-inputs"]]) {
|
||||
const f = assertPermutationStable(`shape-order-unkeyed-unicode-${slug}-forward`, family, "shape-type-mismatch");
|
||||
assert.equal(f.key, null, family);
|
||||
const r = assertPermutationStable(`shape-order-unkeyed-unicode-${slug}-reversed`, family, "shape-type-mismatch");
|
||||
assert.equal(r.key, null, family);
|
||||
// the two fixtures differ only in input order; the string entry is the located one in both
|
||||
const fb = loadBundle(`shape-order-unkeyed-unicode-${slug}-forward`);
|
||||
const rb = loadBundle(`shape-order-unkeyed-unicode-${slug}-reversed`);
|
||||
assert.deepEqual([...fb[family]].reverse(), rb[family], family);
|
||||
for (const bundle of [fb, rb]) {
|
||||
const m = new RegExp(`^bundle\\.${family}\\[(\\d+)\\]`).exec(evaluate(structuredClone(bundle)).detail);
|
||||
assert.ok(m, family);
|
||||
assert.equal(typeof bundle[family][Number(m[1])], "string", `${family}: the located entry is the string`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("R5-1: equal-key Unicode entries — distinct bodies never collapse; first failure is permutation-stable", () => {
|
||||
// records: two agent-a copies (same kind/id/scope/revision, non-ASCII displayName); the copy with the unknown
|
||||
// field sorts first by form ("extra" < "harnessRef"), so shape-unknown-field is reported in every order
|
||||
const agentA = ["agent-definition", "agent-a", "system", 1];
|
||||
const rf = assertPermutationStable("shape-order-equal-key-unicode-records-forward", "records", "shape-unknown-field", agentA);
|
||||
assert.equal(rf.tail, ".payload.extra");
|
||||
assertPermutationStable("shape-order-equal-key-unicode-records-reversed", "records", "shape-unknown-field", agentA);
|
||||
// registries: two ap-full copies (same registry/id/revision/digest, non-ASCII path); same rule in every order
|
||||
const gf = assertPermutationStable("shape-order-equal-key-unicode-registries-forward", "registries", "shape-unknown-field");
|
||||
assert.deepEqual(gf.key.slice(0, 3), ["agent-policy", "ap-full", 1]);
|
||||
assert.equal(gf.tail, ".content.restrictions.extra");
|
||||
assertPermutationStable("shape-order-equal-key-unicode-registries-reversed", "registries", "shape-unknown-field");
|
||||
// the sibling copy's failure (enum) is what remains once the unknown field is removed: the pair really carries two errors
|
||||
const b = loadBundle("shape-order-equal-key-unicode-records-forward");
|
||||
const extra = b.records.find((r) => r.id === "agent-a" && "extra" in r.payload);
|
||||
delete extra.payload.extra;
|
||||
assert.equal(firstFailure(b).rule, "shape-enum-mismatch");
|
||||
const g = loadBundle("shape-order-equal-key-unicode-registries-forward");
|
||||
delete g.registries.find((e) => e.id === "ap-full" && "extra" in e.content.restrictions).content.restrictions.extra;
|
||||
assert.equal(firstFailure(g).rule, "shape-enum-mismatch");
|
||||
// Unicode positives remain allowed
|
||||
assert.equal(firstFailure(loadBundle("positive-unicode-display-name-allowed")).result, "allowed");
|
||||
});
|
||||
|
||||
test("F5: message-is-not-authority follows structural validation and precedes requester admission", () => {
|
||||
const message = { exit: 3, result: "refused", reason: "not-authorized", rule: "message-is-not-authority", step: "message-is-not-authority" };
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-message-precedes-requester-admission")), message);
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-message-is-not-authority")), message);
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-message-precedes-stale-subject")), message);
|
||||
assert.equal(firstFailure(loadBundle("prop-message-after-structural-failure")).rule, "requester-context-mismatch");
|
||||
// without a message the missing delegation keeps its step-3 position
|
||||
assert.deepEqual(firstFailure(loadBundle("prop-delegation-missing-after-requester-admission")),
|
||||
{ exit: 3, result: "refused", reason: "not-authorized", rule: "operation-not-permitted", step: "requester-lacks-original-scope-authority" });
|
||||
assert.equal(firstFailure(loadBundle("prop-delegation-missing")).rule, "delegation-missing");
|
||||
});
|
||||
Reference in New Issue
Block a user