import { describe, expect, it } from "vitest"; import { canonicalize, hash } from "./hash.js"; describe("content hashing", () => { it("canonicalizes object keys lexicographically while preserving array order", () => { expect(canonicalize({ z: 1, a: [{ b: true, a: null }] })).toBe( '{"a":[{"a":null,"b":true}],"z":1}', ); }); it("hashes equivalent object key orders to the same sha256 hex", () => { const left = hash({ z: 1, a: 2 }); const right = hash({ a: 2, z: 1 }); expect(left).toBe(right); expect(left).toMatch(/^[a-f0-9]{64}$/); }); it("preserves own __proto__ keys while canonicalizing objects", () => { const withProtoKey = JSON.parse('{"__proto__":{"x":1},"a":2}') as unknown; expect(canonicalize(withProtoKey)).toBe('{"__proto__":{"x":1},"a":2}'); expect(hash(withProtoKey)).not.toBe(hash({ a: 2 })); }); it("rejects hidden object keys that would be ignored by JSON rendering", () => { const withSymbol = { a: 1, [Symbol("x")]: 2 }; const withHidden = { a: 1 }; Object.defineProperty(withHidden, "hidden", { value: 2, enumerable: false }); expect(() => canonicalize(withSymbol)).toThrow(/non-enumerable or symbol/); expect(() => canonicalize(withHidden)).toThrow(/non-enumerable or symbol/); }); it("rejects non-index array object keys that would be ignored by JSON rendering", () => { const withStringKey = [1] as number[] & { extra?: number }; withStringKey.extra = 2; const withSymbol = [1] as unknown[]; Object.defineProperty(withSymbol, Symbol("x"), { value: 2, enumerable: true }); expect(() => canonicalize(withStringKey)).toThrow(/non-index array/); expect(() => canonicalize(withSymbol)).toThrow(/non-index array/); }); it("renders the shortest round-trippable number literals without plus signs", () => { expect(canonicalize([100, 1000, 11000, 123000, 1e20, 1e21, 0.000001, 0.0000001])).toBe( "[100,1e3,11e3,123e3,1e20,1e21,1e-6,1e-7]", ); }); it("rejects values that are not JSON-safe", () => { const sparse = Array(3); sparse[0] = 1; sparse[2] = 3; expect(() => canonicalize({ date: new Date("2026-05-09T00:00:00Z") })).toThrow( /non-plain object/, ); expect(() => canonicalize({ missing: undefined })).toThrow(/undefined/); expect(() => canonicalize(sparse)).toThrow(/sparse array/); }); });