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.
107 lines
4.1 KiB
JavaScript
107 lines
4.1 KiB
JavaScript
// Unit tests for inspector-content-digest/1 (charter §6, feasibility r2 §6.1 vectors).
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { canonicalize, digestOf, CanonicalError, DIGEST_ALGORITHM } from "./canonical.mjs";
|
|
|
|
const V1 = {
|
|
restrictions: {
|
|
operations: ["work.read", "file.read"],
|
|
readPaths: [{ root: "workspace", path: null }],
|
|
writePaths: [],
|
|
network: "none",
|
|
endpointRefs: [],
|
|
},
|
|
};
|
|
|
|
function refuses(value, code) {
|
|
assert.throws(() => digestOf(value), (e) => e instanceof CanonicalError && e.code === code && e.reason === "unsupported-capability");
|
|
}
|
|
|
|
test("algorithm identifier is fixed", () => {
|
|
assert.equal(DIGEST_ALGORITHM, "inspector-content-digest/1");
|
|
});
|
|
|
|
test("V1: pinned digest of the reader restrictions", () => {
|
|
assert.equal(
|
|
canonicalize(V1),
|
|
'{"restrictions":{"endpointRefs":[],"network":"none","operations":["work.read","file.read"],"readPaths":[{"path":null,"root":"workspace"}],"writePaths":[]}}',
|
|
);
|
|
assert.equal(digestOf(V1), "sha256:0bc44e14fd8354a8a85be879306a54881da7a0cb4d3dcf1f88bc4bc08a40dc9c");
|
|
});
|
|
|
|
test("V2: array order is significant", () => {
|
|
const v2 = structuredClone(V1);
|
|
v2.restrictions.operations = ["file.read", "work.read"];
|
|
assert.equal(digestOf(v2), "sha256:63817bffc57803ca6ac5df971aa0a160d4be73646394b3bc536578bd66c82660");
|
|
assert.notEqual(digestOf(v2), digestOf(V1));
|
|
});
|
|
|
|
test("V3: writer restrictions", () => {
|
|
const v3 = structuredClone(V1);
|
|
v3.restrictions.operations = ["work.read", "file.read", "file.change"];
|
|
v3.restrictions.writePaths = [{ root: "workspace", path: "docs" }];
|
|
assert.equal(digestOf(v3), "sha256:d538c865808cfe7665956615fb48f69819baa0833626d1ef6e1cb3614acbc7da");
|
|
});
|
|
|
|
test("object key order is not significant", () => {
|
|
const reordered = {
|
|
restrictions: {
|
|
writePaths: [], endpointRefs: [], network: "none",
|
|
readPaths: [{ path: null, root: "workspace" }],
|
|
operations: ["work.read", "file.read"],
|
|
},
|
|
};
|
|
assert.equal(digestOf(reordered), digestOf(V1));
|
|
const nullProto = Object.create(null);
|
|
nullProto.restrictions = reordered.restrictions;
|
|
assert.equal(digestOf(nullProto), digestOf(V1));
|
|
});
|
|
|
|
test("V4: non-integral numbers refuse", () => {
|
|
const v4 = structuredClone(V1);
|
|
v4.restrictions.weight = 1.5;
|
|
refuses(v4, "number-not-safe-integer");
|
|
refuses({ a: -0 }, "number-not-safe-integer");
|
|
refuses({ a: 9007199254740992 }, "number-not-safe-integer");
|
|
refuses({ a: Number.NaN }, "number-not-safe-integer");
|
|
refuses({ a: Number.POSITIVE_INFINITY }, "number-not-safe-integer");
|
|
assert.equal(canonicalize({ a: 9007199254740991, b: -5, c: 0 }), '{"a":9007199254740991,"b":-5,"c":0}');
|
|
});
|
|
|
|
test("V5: non-ASCII strings and keys refuse", () => {
|
|
const v5 = structuredClone(V1);
|
|
v5.restrictions.note = "café";
|
|
refuses(v5, "string-not-ascii");
|
|
refuses({ "kéy": 1 }, "string-not-ascii");
|
|
refuses({ a: "\u{1F600}" }, "string-not-ascii");
|
|
});
|
|
|
|
test("ASCII control characters are escaped like Python json.dumps", () => {
|
|
assert.equal(canonicalize({ a: "x\ty\n\"\\" }), '{"a":"x\\ty\\n\\u0001\\"\\\\"}');
|
|
assert.equal(canonicalize({ a: "" }), '{"a":""}');
|
|
});
|
|
|
|
test("keys sort by UTF-16 code unit, not locale", () => {
|
|
assert.equal(canonicalize({ b: 1, B: 2, a: 3, A: 4, _: 5, "10": 6, "2": 7 }), '{"10":6,"2":7,"A":4,"B":2,"_":5,"a":3,"b":1}');
|
|
});
|
|
|
|
test("undefined, functions, symbols, bigints refuse; depth is bounded", () => {
|
|
refuses({ a: undefined }, "value-not-json");
|
|
refuses({ a: () => 1 }, "value-not-json");
|
|
refuses({ a: Symbol("s") }, "value-not-json");
|
|
refuses({ a: 1n }, "value-not-json");
|
|
refuses(undefined, "value-not-json");
|
|
let deep = 1;
|
|
for (let i = 0; i < 64; i += 1) deep = [deep];
|
|
assert.ok(canonicalize(deep));
|
|
deep = [deep];
|
|
refuses(deep, "depth-exceeded");
|
|
});
|
|
|
|
test("booleans, null and nesting encode compactly and deterministically", () => {
|
|
const value = { z: [true, false, null, { y: [], x: {} }] };
|
|
assert.equal(canonicalize(value), '{"z":[true,false,null,{"x":{},"y":[]}]}');
|
|
assert.equal(digestOf(value), digestOf(structuredClone(value)));
|
|
assert.match(digestOf(value), /^sha256:[0-9a-f]{64}$/);
|
|
});
|