import { describe, expect, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { decode } from "../src/decoder/decoder.js"; import { encodeDeterministic } from "../src/deterministic/deterministic.js"; import { encode } from "../src/encoder/encoder.js"; import { decodeSequence, encodeSequence } from "../src/sequences/sequences.js"; /** * Recursive arbitrary covering the JSON-shaped subset of CBOR values that * the encoder accepts and the decoder produces verbatim. Excludes: * - `undefined` (excluded by deterministic-mode round-trip tests) * - `Map` / `Tagged` / `bigint` (covered by targeted unit tests) * - Float values that aren't exactly representable in float32 (the * default codec selects float64, which round-trips, but our equality * check uses `toEqual` so we exclude NaN). */ const cborValue = (): fc.Arbitrary => { const leaves: fc.Arbitrary = fc.oneof( fc.constant(null), fc.boolean(), fc.integer({ min: -1_000_000, max: 1_000_000 }), fc.string(), fc.uint8Array({ minLength: 0, maxLength: 32 }) ); return fc.letrec((tie) => ({ v: fc.oneof({ depthSize: "small" }, leaves, tie("array"), tie("obj")), array: fc.array(tie("v"), { maxLength: 6 }), obj: fc.dictionary(fc.string(), tie("v"), { maxKeys: 6 }), })).v; }; describe("encode + decode round-trip", () => { it("default mode preserves arbitrary JSON-shaped values", { timeout: 30000, }, () => { fc.assert( fc.property(cborValue(), (value) => { const wire = encode(value); const back = decode(wire); // CBOR maps decode as `Map`; plain objects in our arbitrary // become a Map after decode. Compare via JSON-shaped projection. expect(toComparable(back)).toEqual(toComparable(value)); }), { numRuns: 200 } ); }); it("deterministic mode is byte-stable across two runs on the same input", { timeout: 30000, }, () => { fc.assert( fc.property(cborValue(), (value) => { const a = encodeDeterministic(value); const b = encodeDeterministic(value); expect(Array.from(a)).toEqual(Array.from(b)); }), { numRuns: 200 } ); }); it("deterministic mode produces byte-identical output for shuffled-key objects", { timeout: 30000, }, () => { fc.assert( fc.property( fc.dictionary( fc.string({ minLength: 1, maxLength: 8 }), fc.integer(), { minKeys: 1, maxKeys: 8, } ), (obj) => { const keys = Object.keys(obj); if (keys.length < 2) return; // Build a key-shuffled copy const shuffled: Record = {}; for (const k of [...keys].reverse()) shuffled[k] = obj[k] as number; expect(Array.from(encodeDeterministic(obj))).toEqual( Array.from(encodeDeterministic(shuffled)) ); } ), { numRuns: 100 } ); }); }); describe("CBOR Sequences round-trip", () => { it("encode + decode preserves mixed-type item lists", { timeout: 30000, }, () => { fc.assert( fc.property( fc.array(cborValue(), { minLength: 0, maxLength: 8 }), (items) => { const wire = encodeSequence(items); const back = decodeSequence(wire); expect(back.map(toComparable)).toEqual( items.map(toComparable) ); } ), { numRuns: 100 } ); }); }); /** * Project decoded values back into a comparable JSON-shaped form so * `Map`-decoded objects equate to their plain-object inputs. */ function toComparable(v: unknown): unknown { if (v === null || typeof v !== "object") return v; if (v instanceof Uint8Array) return Array.from(v); if (Array.isArray(v)) return v.map(toComparable); if (v instanceof Map) { const out: Record = {}; for (const [k, val] of v) out[String(k)] = toComparable(val); return out; } const out: Record = {}; for (const [k, val] of Object.entries(v as Record)) { out[k] = toComparable(val); } return out; }