Files
suaveplan/packages/foundation/flow/cbor/tests/property.test.ts
2026-05-27 23:47:53 +01:00

130 lines
4.6 KiB
TypeScript

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<unknown> => {
const leaves: fc.Arbitrary<unknown> = 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<string, number> = {};
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<string, unknown> = {};
for (const [k, val] of v) out[String(k)] = toComparable(val);
return out;
}
const out: Record<string, unknown> = {};
for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
out[k] = toComparable(val);
}
return out;
}