// 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}$/); });