import { BitPacker, type CustomTypeHandler, CustomTypeRegistry, type PackingContext, } from "@suaveplan/bitpacker"; import { describe, expect, it } from "@suaveplan/testing/runner"; import type { BitPackerSchema } from "@suaveplan/types"; import { registerDefaultStrategies } from "../register-default-strategies/register-default-strategies.js"; import { ArrayPackingStrategy } from "../strategies/array/array.js"; import { ArrayConfigPackingStrategy, isArrayConfig, validateArrayData, } from "../strategies/array-config/array-config.js"; import { ArrayNotationPackingStrategy, getArrayLength, getElementSchema, parseArraySchema, } from "../strategies/array-notation/array-notation.js"; import { ArraySchemaPackingStrategy } from "../strategies/array-schema/array-schema.js"; import { BigIntPackingStrategy } from "../strategies/bigint/bigint.js"; import { BitsetPackingStrategy } from "../strategies/bitset/bitset.js"; import { ConditionalPackingStrategy, evaluatePredicate, isConditionalSchema, } from "../strategies/conditional/conditional.js"; import { DateTimePackingStrategy } from "../strategies/datetime/datetime.js"; import { EnumPackingStrategy } from "../strategies/enum/enum.js"; import { MapPackingStrategy } from "../strategies/map/map.js"; import { NumberPackingStrategy } from "../strategies/number/number.js"; import { OneOfPackingStrategy } from "../strategies/oneOf/oneOf.js"; import { OptionalSuffixPackingStrategy } from "../strategies/optional-suffix/optional-suffix.js"; import { SetPackingStrategy } from "../strategies/set/set.js"; import { StringPackingStrategy } from "../strategies/string/string.js"; import { UnionPackingStrategy } from "../strategies/union/union.js"; function createBitPacker(): BitPacker { const bp = new BitPacker(); registerDefaultStrategies(bp.strategyManager); return bp; } describe("Default strategies", () => { describe("number", () => { it("round-trips unsigned integer", () => { const bp = createBitPacker(); const packed = bp.pack(42, 255); expect(bp.unpack(packed, 255)).toBe(42); }); it("round-trips signed integer", () => { const bp = createBitPacker(); const packed = bp.pack(-50, -127); expect(bp.unpack(packed, -127)).toBe(-50); }); it("round-trips fixed-point", () => { const bp = createBitPacker(); const packed = bp.pack(1.5, 4.2); expect(bp.unpack(packed, 4.2)).toBeCloseTo(1.5, 2); }); it("round-trips schema 0", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(0, 0), 0)).toBe(0); }); it("throws for schema 0 with non-zero value", () => { const bp = createBitPacker(); expect(() => bp.pack(1, 0)).toThrow(); }); it("throws for value out of range unsigned", () => { const bp = createBitPacker(); expect(() => bp.pack(300, 255)).toThrow(); }); it("throws for value out of range signed", () => { const bp = createBitPacker(); expect(() => bp.pack(-200, -127)).toThrow(); }); it("round-trips signed negative value", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(-100, -127), -127)).toBe(-100); }); it("round-trips positive signed integer", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(50, -127), -127)).toBe(50); }); it("round-trips signed fixed-point number schema", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(-1.5, -4.2), -4.2)).toBeCloseTo(-1.5, 2); }); it("throws for fixed-point value out of range", () => { const bp = createBitPacker(); expect(() => bp.pack(1000, 4.2)).toThrow(); }); it("packs signed fixed-point directly", () => { const strategy = new NumberPackingStrategy(); const writer = { writeInt: () => {}, writeUint: () => {} }; const context = { writer } as unknown as PackingContext; strategy.pack(-1.5, -4.2, context); }); }); describe("string", () => { it("round-trips fixed-length string", () => { const bp = createBitPacker(); const packed = bp.pack("hello", "8"); expect(bp.unpack(packed, "8")).toBe("hello"); }); it("round-trips variable-length string", () => { const bp = createBitPacker(); const packed = bp.pack("hi", "-16"); expect(bp.unpack(packed, "-16")).toBe("hi"); }); it("round-trips UUID", () => { const bp = createBitPacker(); const uuid = "550e8400-e29b-41d4-a716-446655440000"; const packed = bp.pack(uuid, "uuid"); expect(bp.unpack(packed, "uuid")).toBe(uuid); }); it("round-trips UUID as Uint8Array", () => { const bp = createBitPacker(); const uuid = "550e8400-e29b-41d4-a716-446655440000"; const bytes = new Uint8Array(16); const hex = uuid.replace(/-/g, ""); for (let i = 0; i < 16; i++) { bytes[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16); } expect(bp.unpack(bp.pack(bytes, "uuid"), "uuid")).toBe(uuid); }); it("throws for invalid UUID string", () => { const bp = createBitPacker(); expect(() => bp.pack("not-a-uuid", "uuid")).toThrow(); }); it("throws for invalid UUID type", () => { const bp = createBitPacker(); expect(() => bp.pack(12345, "uuid")).toThrow(); }); it("round-trips f32", () => { const bp = createBitPacker(); const packed = bp.pack(3.14, "f32"); expect(bp.unpack(packed, "f32")).toBeCloseTo(3.14, 5); }); it("round-trips f64", () => { const bp = createBitPacker(); const packed = bp.pack(Math.E, "f64"); expect(bp.unpack(packed, "f64")).toBe(Math.E); }); it("throws for float non-number", () => { const bp = createBitPacker(); expect(() => bp.pack("pi", "f32")).toThrow(); }); it("throws for fixed string non-string", () => { const bp = createBitPacker(); expect(() => bp.pack(42, "8")).toThrow(); }); it("throws for fixed string too long", () => { const bp = createBitPacker(); expect(() => bp.pack("this is way too long", "4")).toThrow(); }); it("round-trips fixed-float string schema", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(1.5, "4.2"), "4.2")).toBeCloseTo(1.5, 2); }); it("round-trips signed fixed-float string schema", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(-1.5, "-4.2"), "-4.2")).toBeCloseTo( -1.5, 2 ); }); it("round-trips fixed-float with string value", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack("1.5", "4.2"), "4.2")).toBeCloseTo(1.5, 2); }); it("round-trips empty string with schema 0", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack("", "0"), "0")).toBe(""); }); it("packs variable string -0 directly", () => { const strategy = new StringPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer } as unknown as PackingContext; strategy.pack("", "-0", context); }); it("unpacks variable string -0 directly", () => { const strategy = new StringPackingStrategy(); const reader = { readUint: () => 0 }; const context = { reader } as unknown as PackingContext; expect(strategy.unpack("-0", context)).toBe(""); }); }); describe("boolean", () => { it("round-trips true", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(true, true), true)).toBe(true); }); it("round-trips false", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(false, true), true)).toBe(false); }); }); describe("null", () => { it("packs null with zero bits", () => { const bp = createBitPacker(); const packed = bp.pack(null, null); expect(packed.length).toBe(0); expect(bp.unpack(packed, null)).toBe(null); }); }); describe("bigint", () => { it("round-trips signed bigint", () => { const bp = createBitPacker(); const packed = bp.pack(BigInt(-9007199254740993), "bigint-i64"); expect(bp.unpack(packed, "bigint-i64")).toBe( BigInt(-9007199254740993) ); }); it("round-trips unsigned bigint", () => { const bp = createBitPacker(); const packed = bp.pack(BigInt(18446744073709551615n), "bigint-u64"); expect(bp.unpack(packed, "bigint-u64")).toBe( BigInt(18446744073709551615n) ); }); it("round-trips bigint array", () => { const bp = createBitPacker(); const values = [1n, 2n, 3n]; const packed = bp.pack(values, "bigint-i64[]"); expect(bp.unpack(packed, "bigint-i64[]")).toEqual(values); }); it("throws for non-bigint data", () => { const bp = createBitPacker(); expect(() => bp.pack(42, "bigint-i64")).toThrow(); }); it("packs bigint array directly", () => { const strategy = new BigIntPackingStrategy(); const writer = { writeBytes: () => {}, writeUint: () => {}, }; const context = { writer } as unknown as PackingContext; strategy.pack([1n, 2n], "bigint-i64[]", context); }); it("unpacks bigint array directly", () => { const strategy = new BigIntPackingStrategy(); let _calls = 0; const reader = { readBytes: () => { _calls++; const buf = new ArrayBuffer(8); const view = new DataView(buf); view.setBigInt64(0, 42n, false); return new Uint8Array(buf); }, readUint: () => 2, }; const context = { reader } as unknown as PackingContext; const result = strategy.unpack("bigint-i64[]", context); expect(result).toEqual([42n, 42n]); }); it("throws for non-array bigint array data", () => { const strategy = new BigIntPackingStrategy(); const context = {} as unknown as PackingContext; expect(() => strategy.pack("not array", "bigint-i64[]", context) ).toThrow(); }); }); describe("bigint-array", () => { it("round-trips BigInt64Array", () => { const bp = createBitPacker(); const data = new BigInt64Array([1n, 2n, 3n]); expect( bp.unpack(bp.pack(data, "bigint-i64[]"), "bigint-i64[]") ).toEqual([1n, 2n, 3n]); }); it("round-trips BigUint64Array", () => { const bp = createBitPacker(); const data = new BigUint64Array([1n, 2n, 3n]); expect( bp.unpack(bp.pack(data, "bigint-u64[]"), "bigint-u64[]") ).toEqual([1n, 2n, 3n]); }); it("throws for array too long", () => { const bp = createBitPacker(); const data = new Array(65536).fill(0n); expect(() => bp.pack(data, "bigint-i64[]")).toThrow(); }); it("throws for non-array data", () => { const bp = createBitPacker(); expect(() => bp.pack("bad", "bigint-i64[]")).toThrow(); }); }); describe("object", () => { it("round-trips flat object", () => { const bp = createBitPacker(); const schema = { x: 255, y: "-16", flag: true }; const data = { x: 100, y: "hello", flag: false }; const packed = bp.pack(data, schema); expect(bp.unpack(packed, schema)).toEqual(data); }); it("round-trips nested object", () => { const bp = createBitPacker(); const schema = { pos: { x: 255, y: 255 }, name: "-8", }; const data = { pos: { x: 10, y: 20 }, name: "A" }; const packed = bp.pack(data, schema); expect(bp.unpack(packed, schema)).toEqual(data); }); it("throws for non-object data", () => { const bp = createBitPacker(); expect(() => bp.pack("bad", { x: 255 })).toThrow(); }); it("handles undefined schema fields gracefully", () => { const bp = createBitPacker(); const schema = { x: undefined, y: 255, } as unknown as BitPackerSchema; const data = { y: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual({ y: 10 }); }); it("handles missing object keys", () => { const bp = createBitPacker(); const schema = { x: 255, y: { _optional: 255 } }; const data = { x: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual({ x: 10, y: undefined, }); }); }); describe("array", () => { it("round-trips variable-length array", () => { const bp = createBitPacker(); const schema: BitPackerSchema = [255]; const data = [1, 2, 3]; const packed = bp.pack(data, schema); expect(bp.unpack(packed, schema)).toEqual(data); }); it("round-trips array with max length", () => { const bp = createBitPacker(); const schema: BitPackerSchema = [10, 255]; const data = [5, 6]; const packed = bp.pack(data, schema); expect(bp.unpack(packed, schema)).toEqual(data); }); it("throws for non-array data", () => { const bp = createBitPacker(); expect(() => bp.pack("not array", [255])).toThrow(); }); it("throws when array exceeds max length", () => { const bp = createBitPacker(); const schema = [2, 255]; expect(() => bp.pack([1, 2, 3], schema)).toThrow(); }); it("packs array with maxLength 0 directly", () => { const strategy = new ArrayPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack([], [0, 255], context); }); }); describe("array-schema (tuple)", () => { it("round-trips tuple array", () => { const bp = createBitPacker(); const schema = [255, "-8", true]; const data = [42, "hi", false]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("throws when tuple schema item is undefined", () => { const strategy = new ArraySchemaPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; const schema = [255, undefined, true] as unknown as BitPackerSchema; expect(() => strategy.pack([1, 2, 3], schema, context)).toThrow(); }); it("packs tuple with non-array data", () => { const strategy = new ArraySchemaPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack("not array", [255], context); }); }); describe("array-config", () => { it("round-trips integer array config", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "integer", elementMax: 10, }; const data = [1, 2, 3]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips float array config", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "float", elementMax: 100, }; const data = [1, 2, 3]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips boolean array config", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "boolean", }; const data = [true, false]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips schema array config", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "schema", elementSchema: 255, }; const data = [1, 2, 3]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips string array config", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 3, elementType: "string", elementMax: 4, }; const data = ["hi", "ho"]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips integer array config without elementMax", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "integer", }; const data = [1, 2, 3]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips float array config without elementMax", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "float", }; const data = [1, 2, 3]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("round-trips string array config without elementMax", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 2, elementType: "string", }; const data = ["hi", "ho"]; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("throws for non-array data", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "integer", }; expect(() => bp.pack("bad", schema)).toThrow(); }); it("throws for invalid element data", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { type: "array", maxLength: 5, elementType: "integer", }; expect(() => bp.pack([1.5], schema)).toThrow(); }); it("isArrayConfig rejects non-objects", () => { expect(isArrayConfig("bad")).toBe(false); expect(isArrayConfig(null)).toBe(false); expect(isArrayConfig([])).toBe(false); }); it("isArrayConfig rejects missing fields", () => { expect(isArrayConfig({ type: "array" })).toBe(false); expect( isArrayConfig({ type: "array", maxLength: 5, elementType: "bad", }) ).toBe(false); expect( isArrayConfig({ type: "array", maxLength: 5, elementType: "schema", }) ).toBe(false); }); it("validateArrayData handles default case", () => { expect( validateArrayData([1], { type: "array", maxLength: 5, elementType: "bad", } as unknown as Parameters[1]) ).toBe(false); }); it("validateArrayData rejects non-array", () => { expect( validateArrayData( "bad" as unknown as unknown[], { type: "array", maxLength: 5, elementType: "integer", } as unknown as Parameters[1] ) ).toBe(false); }); it("validateArrayData rejects too long array", () => { expect( validateArrayData([1, 2, 3, 4, 5, 6], { type: "array", maxLength: 5, elementType: "integer", } as unknown as Parameters[1]) ).toBe(false); }); it("packs array config with maxLength 0 directly", () => { const strategy = new ArrayConfigPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack( [], { type: "array", maxLength: 0, elementType: "integer", } as unknown as BitPackerSchema, context ); }); }); describe("array-notation", () => { it("round-trips integer elements", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack([1, 2, 3], "-5[10]"), "-5[10]")).toEqual([ 1, 2, 3, ]); }); it("round-trips float elements", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack([1, 2], "-5[4.2]"), "-5[4.2]")).toEqual([ 1, 2, ]); }); it("round-trips boolean elements", () => { const bp = createBitPacker(); expect( bp.unpack(bp.pack([true, false], "-5[true]"), "-5[true]") ).toEqual([true, false]); }); it("round-trips object elements", () => { const bp = createBitPacker(); expect( bp.unpack( bp.pack([{ x: 1 }, { x: 2 }], '-5[{"x":10}]'), '-5[{"x":10}]' ) ).toEqual([{ x: 1 }, { x: 2 }]); }); it("round-trips nested array notation", () => { const bp = createBitPacker(); expect( bp.unpack(bp.pack([[1], [2]], "-5[-3[10]]"), "-5[-3[10]]") ).toEqual([[1], [2]]); }); it("throws for invalid array notation schema", () => { const bp = createBitPacker(); expect(() => bp.pack([1], "-5[unknown]")).toThrow(); }); it("throws for non-array data", () => { const bp = createBitPacker(); expect(() => bp.pack("bad", "-5[10]")).toThrow(); }); it("throws when data too long", () => { const bp = createBitPacker(); expect(() => bp.pack([1, 2, 3, 4, 5, 6], "-3[10]")).toThrow(); }); it("getArrayLength parses length", () => { expect(getArrayLength("-5[10]")).toBe(5); }); it("getArrayLength throws for invalid schema", () => { expect(() => getArrayLength("bad")).toThrow(); }); it("getElementSchema extracts inner schema", () => { expect(getElementSchema("-5[10]")).toBe("10"); }); it("getElementSchema throws for invalid schema", () => { expect(() => getElementSchema("bad")).toThrow(); }); it("parseArraySchema returns null for invalid notation", () => { expect(parseArraySchema("bad")).toBeNull(); }); it("parseArraySchema handles nested arrays", () => { const result = parseArraySchema("-5[-3[10]]"); expect(result).not.toBeNull(); expect(result?.elementType).toBe("schema"); }); it("parseArraySchema handles object schemas", () => { const result = parseArraySchema('-5[{"x":10}]'); expect(result).not.toBeNull(); expect(result?.elementType).toBe("schema"); }); it("parseArraySchema returns null for invalid JSON object", () => { expect(parseArraySchema("-5[{bad}]")).toBeNull(); }); it("throws on unpack for invalid array notation schema", () => { const strategy = new ArrayNotationPackingStrategy(); const reader = { readUint: () => 0 }; const context = { reader } as unknown as PackingContext; expect(() => strategy.unpack("-5[unknown]", context)).toThrow(); }); it("packs array notation with maxLength 0 directly", () => { const strategy = new ArrayNotationPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack([], "-0[10]", context); }); it("unpacks array notation with maxLength 0 directly", () => { const strategy = new ArrayNotationPackingStrategy(); const reader = { readUint: () => 0 }; const context = { reader } as unknown as PackingContext; expect(strategy.unpack("-0[10]", context)).toEqual([]); }); }); describe("enum", () => { it("round-trips enum value", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _enum: ["red", "green", "blue"] }; const packed = bp.pack("green", schema); expect(bp.unpack(packed, schema)).toBe("green"); }); it("packs single-value enum directly", () => { const strategy = new EnumPackingStrategy(); const writer = { writeUint: () => {} }; const context = { writer } as unknown as PackingContext; strategy.pack("only", { _enum: ["only"] }, context); }); it("throws for non-string data", () => { const bp = createBitPacker(); const schema = { _enum: ["a", "b"] }; expect(() => bp.pack(42, schema)).toThrow(); }); it("throws for unknown enum value", () => { const bp = createBitPacker(); const schema = { _enum: ["a", "b"] }; expect(() => bp.pack("c", schema)).toThrow(); }); it("throws on unpack when index out of range", () => { const strategy = new EnumPackingStrategy(); const reader = { readUint: () => 5 }; const context = { reader } as unknown as PackingContext; expect(() => strategy.unpack({ _enum: ["a", "b"] }, context) ).toThrow(); }); }); describe("optional", () => { it("round-trips present optional", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _optional: 255 }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("round-trips absent optional", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _optional: 255 }; expect( bp.unpack(bp.pack(undefined, schema), schema) ).toBeUndefined(); }); }); describe("optional-marker", () => { it("round-trips present value", () => { const bp = createBitPacker(); const schema = { _optional: true, _schema: 255 }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("returns undefined when absent", () => { const bp = createBitPacker(); const schema = { _optional: true, _schema: 255 }; expect( bp.unpack(bp.pack(undefined, schema), schema) ).toBeUndefined(); }); it("returns undefined when null", () => { const bp = createBitPacker(); const schema = { _optional: true, _schema: 255 }; expect(bp.unpack(bp.pack(null, schema), schema)).toBeUndefined(); }); it("returns default when absent", () => { const bp = createBitPacker(); const schema = { _optional: true, _schema: 255, _default: 99 }; expect(bp.unpack(bp.pack(undefined, schema), schema)).toBe(99); }); }); describe("optional-suffix", () => { it("round-trips uuid?", () => { const bp = createBitPacker(); const uuid = "550e8400-e29b-41d4-a716-446655440000"; expect(bp.unpack(bp.pack(uuid, "uuid?"), "uuid?")).toBe(uuid); }); it("returns undefined for absent uuid?", () => { const bp = createBitPacker(); expect( bp.unpack(bp.pack(undefined, "uuid?"), "uuid?") ).toBeUndefined(); }); it("round-trips fixed string 8?", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack("hello", "8?"), "8?")).toBe("hello"); }); it("round-trips variable string -8?", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack("hi", "-8?"), "-8?")).toBe("hi"); }); it("round-trips varint?", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(150, "varint?"), "varint?")).toBe(150); }); it("isOptionalSuffix rejects non-string", () => { expect(OptionalSuffixPackingStrategy.isOptionalSuffix(42)).toBe( false ); }); it("isOptionalSuffix rejects missing base", () => { expect(OptionalSuffixPackingStrategy.isOptionalSuffix("?")).toBe( false ); }); it("isOptionalSuffix rejects double suffix", () => { expect( OptionalSuffixPackingStrategy.isOptionalSuffix("hello??") ).toBe(false); }); it("baseSchema strips suffix", () => { expect(OptionalSuffixPackingStrategy.baseSchema("hello?")).toBe( "hello" ); }); }); describe("oneOf", () => { it("round-trips string branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: ["-8", 255] }; expect(bp.unpack(bp.pack("hello", schema), schema)).toBe("hello"); }); it("round-trips number branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: ["-8", 255] }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("round-trips boolean branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [true, 255] }; expect(bp.unpack(bp.pack(false, schema), schema)).toBe(false); }); it("round-trips null branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [null, 255] }; expect(bp.unpack(bp.pack(null, schema), schema)).toBe(null); }); it("round-trips array branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [[255], "-8"] }; expect(bp.unpack(bp.pack([1, 2], schema), schema)).toEqual([1, 2]); }); it("round-trips object branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [{ x: 255 }, "-8"] }; expect(bp.unpack(bp.pack({ x: 5 }, schema), schema)).toEqual({ x: 5, }); }); it("round-trips nested oneOf branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [{ _oneOf: [true, 255] }, "-8"], }; expect(bp.unpack(bp.pack(true, schema), schema)).toBe(true); }); it("round-trips optional branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [{ _optional: 255 }, "-8"], }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("round-trips enum branch", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [{ _enum: ["a", "b"] }, "-8"], }; expect(bp.unpack(bp.pack("a", schema), schema)).toBe("a"); }); it("throws when no branch matches", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _oneOf: [true, 255] }; expect(() => bp.pack("hello", schema)).toThrow(); }); it("throws on unpack when index out of range", () => { const strategy = new OneOfPackingStrategy(); const reader = { readUint: () => 5 }; const context = { reader, unpackValue: () => {}, } as unknown as PackingContext; expect(() => strategy.unpack({ _oneOf: [255, "-8"] }, context) ).toThrow(); }); it("#canPack returns false for unsupported schema types", () => { const bp = createBitPacker(); const schema = { _oneOf: [123n as unknown as BitPackerSchema, 255], }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("packs oneOf with single branch directly", () => { const strategy = new OneOfPackingStrategy(); const writer = { writeUint: () => {} }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack(42, { _oneOf: [255] }, context); }); }); describe("varint", () => { it("round-trips unsigned varint", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(150, "varint"), "varint")).toBe(150); }); it("round-trips signed varint", () => { const bp = createBitPacker(); expect(bp.unpack(bp.pack(-150, "-varint"), "-varint")).toBe(-150); }); it("throws for non-integer data", () => { const bp = createBitPacker(); expect(() => bp.pack(1.5, "varint")).toThrow(); }); it("throws for negative unsigned varint", () => { const bp = createBitPacker(); expect(() => bp.pack(-1, "varint")).toThrow(); }); }); describe("map", () => { it("round-trips Map", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _map: { key: "-8", value: 255, maxSize: 10 }, }; const data = new Map([ ["a", 1], ["b", 2], ]); const unpacked = bp.unpack(bp.pack(data, schema), schema) as Map< string, number >; expect(Array.from(unpacked.entries())).toEqual([ ["a", 1], ["b", 2], ]); }); it("throws for non-Map data", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _map: { key: "-8", value: 255, maxSize: 10 }, }; expect(() => bp.pack({ a: 1 }, schema)).toThrow(); }); it("throws when map exceeds maxSize", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _map: { key: 255, value: 255, maxSize: 2 }, }; expect(() => bp.pack( new Map([ [1, 1], [2, 2], [3, 3], ]), schema ) ).toThrow(); }); it("packs map with maxSize 0 directly", () => { const strategy = new MapPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack( new Map(), { _map: { key: "-8", value: 255, maxSize: 0 } }, context ); }); }); describe("set", () => { it("round-trips Set", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _set: { element: 255, maxSize: 10 }, }; const data = new Set([1, 2, 3]); const unpacked = bp.unpack( bp.pack(data, schema), schema ) as Set; expect(Array.from(unpacked)).toEqual([1, 2, 3]); }); it("throws for non-Set data", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _set: { element: 255, maxSize: 10 }, }; expect(() => bp.pack([1, 2], schema)).toThrow(); }); it("throws when set exceeds maxSize", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _set: { element: 255, maxSize: 2 }, }; expect(() => bp.pack(new Set([1, 2, 3]), schema)).toThrow(); }); it("packs set with maxSize 0 directly", () => { const strategy = new SetPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer, packValue: () => {}, } as unknown as PackingContext; strategy.pack( new Set(), { _set: { element: 255, maxSize: 0 } }, context ); }); }); // Adaptive blob `"~"` is now covered by the co-located // `strategies/blob/blob.test.ts` per the bitpacker-canonical-spec. describe("bitarray", () => { it("round-trips variable-length bitArray", () => { const bp = createBitPacker(); const data = [true, false, true]; expect(bp.unpack(bp.pack(data, "bitArray"), "bitArray")).toEqual( data ); }); it("round-trips fixed-length bitArray", () => { const bp = createBitPacker(); const data = [true, false, true, false, true]; const schema = { _bitArray: { length: 5 } }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("throws for non-array data", () => { const bp = createBitPacker(); expect(() => bp.pack("bad", "bitArray")).toThrow(); }); it("throws for non-boolean array", () => { const bp = createBitPacker(); expect(() => bp.pack([1, 2], "bitArray")).toThrow(); }); it("throws for fixed length mismatch", () => { const bp = createBitPacker(); const schema = { _bitArray: { length: 3 } }; expect(() => bp.pack([true, false], schema)).toThrow(); }); }); describe("bitset", () => { it("round-trips bitset", () => { const bp = createBitPacker(); const schema = { _bitset: { maxSize: 10 } }; const data = new Set([0, 2, 4]); expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("throws for non-Set data", () => { const bp = createBitPacker(); expect(() => bp.pack([0, 2], { _bitset: { maxSize: 10 } }) ).toThrow(); }); it("throws when set exceeds maxSize", () => { const bp = createBitPacker(); expect(() => bp.pack(new Set([0, 1, 2, 3]), { _bitset: { maxSize: 2 } }) ).toThrow(); }); it("packs bitset with maxSize 0 directly", () => { const strategy = new BitsetPackingStrategy(); const writer = { writeUint: () => {}, writeInt: () => {}, writeBool: () => {}, writeBytes: () => {}, }; const context = { writer } as unknown as PackingContext; strategy.pack(new Set(), { _bitset: { maxSize: 0 } }, context); }); }); describe("datetime direct", () => { function createMockContext() { const calls: unknown[] = []; const writer = { writeInt: (v: number, b: number) => calls.push(["writeInt", v, b]), writeUint: (v: number, b: number) => calls.push(["writeUint", v, b]), writeBool: (v: boolean) => calls.push(["writeBool", v]), writeBytes: (v: Uint8Array) => calls.push(["writeBytes", v]), }; return { calls, writer }; } it("packs timestamp with Date", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); const date = new Date("2024-01-15T10:30:00.000Z"); strategy.pack(date, "timestamp", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", date.getTime(), 53]]); }); it("packs timestamp with number", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(12345, "timestamp", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 12345, 53]]); }); it("packs timestamp-ms", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(12345, "timestamp-ms", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 12345, 53]]); }); it("packs date-only", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(12345, "date-only", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 12345, 53]]); }); it("packs timestamp-s", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(5000, "timestamp-s", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 5, 32]]); }); it("packs timespan", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(3600, "timespan", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 3600, 53]]); }); it("packs timespan-ms", () => { const strategy = new DateTimePackingStrategy(); const { calls, writer } = createMockContext(); strategy.pack(3600, "timespan-ms", { writer, } as unknown as PackingContext); expect(calls).toEqual([["writeInt", 3600, 53]]); }); it("throws for invalid data type", () => { const strategy = new DateTimePackingStrategy(); const { writer } = createMockContext(); expect(() => strategy.pack("bad", "timestamp", { writer, } as unknown as PackingContext) ).toThrow(); }); it("unpacks timestamp", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 12345 }; expect( strategy.unpack("timestamp", { reader, } as unknown as PackingContext) ).toBe(12345); }); it("unpacks timestamp-ms", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 12345 }; expect( strategy.unpack("timestamp-ms", { reader, } as unknown as PackingContext) ).toBe(12345); }); it("unpacks date-only", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 12345 }; expect( strategy.unpack("date-only", { reader, } as unknown as PackingContext) ).toBe(12345); }); it("unpacks timestamp-s", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 5 }; expect( strategy.unpack("timestamp-s", { reader, } as unknown as PackingContext) ).toBe(5000); }); it("unpacks timespan", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 3600 }; expect( strategy.unpack("timespan", { reader, } as unknown as PackingContext) ).toBe(3600); }); it("unpacks timespan-ms", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 3600 }; expect( strategy.unpack("timespan-ms", { reader, } as unknown as PackingContext) ).toBe(3600); }); it("unpack returns 0 for unknown token", () => { const strategy = new DateTimePackingStrategy(); const reader = { readInt: () => 0 }; expect( strategy.unpack( "unknown" as unknown as BitPackerSchema, { reader, } as unknown as PackingContext ) ).toBe(0); }); }); describe("defs", () => { it("round-trips via schema registry", () => { const bp = new BitPacker({ schemaRegistry: new Map([ ["player", { name: "-8", health: 255 }], ]), }); registerDefaultStrategies(bp.strategyManager); const schema = { _defs: { name: "player" } }; const data = { name: "Alice", health: 100 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); }); describe("version", () => { it("round-trips versioned schema", () => { const bp = createBitPacker(); const schema = { _version: 1, _schema: 255 }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("throws on strict mismatch", () => { const bp = createBitPacker(); const schema = { _version: 1, _schema: 255 }; const packed = bp.pack(42, schema); const badSchema = { _version: 2, _schema: 255 }; expect(() => bp.unpack(packed, badSchema)).toThrow(); }); it("accepts a listed version when _tolerance: forward", () => { const bp = createBitPacker(); const writeSchema = { _version: 1, _schema: 255 }; const packed = bp.pack(42, writeSchema); const readSchema = { _version: 2, _schema: 255, _tolerance: "forward" as const, _allowedVersions: [1, 2, 3], }; expect(bp.unpack(packed, readSchema)).toBe(42); }); it("accepts any version when _tolerance: forward without allowlist", () => { const bp = createBitPacker(); const writeSchema = { _version: 7, _schema: 255 }; const packed = bp.pack(42, writeSchema); const readSchema = { _version: 1, _schema: 255, _tolerance: "forward" as const, }; expect(bp.unpack(packed, readSchema)).toBe(42); }); it("rejects an unlisted version under _tolerance: forward", () => { const bp = createBitPacker(); const writeSchema = { _version: 9, _schema: 255 }; const packed = bp.pack(42, writeSchema); const readSchema = { _version: 1, _schema: 255, _tolerance: "forward" as const, _allowedVersions: [1, 2, 3], }; expect(() => bp.unpack(packed, readSchema)).toThrow(); }); }); describe("conditional (_when / _if)", () => { it("selects branch based on parent data with _when", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { active: true, payload: { _when: { field: "active", op: "==", value: true }, _then: { level: 255 }, _else: { name: "-8" }, }, }; const activeData = { active: true, payload: { level: 5 } }; const inactiveData = { active: false, payload: { name: "idle" } }; expect(bp.unpack(bp.pack(activeData, schema), schema)).toEqual( activeData ); expect(bp.unpack(bp.pack(inactiveData, schema), schema)).toEqual( inactiveData ); }); it("selects branch based on _if predicate", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { active: true, payload: { _if: { field: "active", op: "==", value: true }, _then: { level: 255 }, _else: { name: "-8" }, }, }; const activeData = { active: true, payload: { level: 5 } }; const inactiveData = { active: false, payload: { name: "idle" } }; expect(bp.unpack(bp.pack(activeData, schema), schema)).toEqual( activeData ); expect(bp.unpack(bp.pack(inactiveData, schema), schema)).toEqual( inactiveData ); }); it("handles missing _else by returning undefined", () => { const bp = createBitPacker(); const schema = { active: true, payload: { _if: { field: "active", op: "==", value: true }, _then: { level: 255 }, }, }; const inactive = { active: false, payload: undefined }; expect(bp.unpack(bp.pack(inactive, schema), schema)).toEqual({ active: false, payload: undefined, }); }); it("uses != op", () => { const bp = createBitPacker(); const schema = { mode: "-8", payload: { _when: { field: "mode", op: "!=", value: "A" }, _then: { level: 255 }, }, }; const data = { mode: "B", payload: { level: 10 } }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses < op", () => { const bp = createBitPacker(); const schema = { level: 255, bonus: { _when: { field: "level", op: "<", value: 100 }, _then: 255, }, }; const data = { level: 50, bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses <= op", () => { const bp = createBitPacker(); const schema = { level: 255, bonus: { _when: { field: "level", op: "<=", value: 50 }, _then: 255, }, }; const data = { level: 50, bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses > op", () => { const bp = createBitPacker(); const schema = { level: 255, bonus: { _when: { field: "level", op: ">", value: 50 }, _then: 255, }, }; const data = { level: 100, bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses >= op", () => { const bp = createBitPacker(); const schema = { level: 255, bonus: { _when: { field: "level", op: ">=", value: 100 }, _then: 255, }, }; const data = { level: 100, bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses in op", () => { const bp = createBitPacker(); const schema = { mode: "-8", bonus: { _when: { field: "mode", op: "in", value: ["A", "B"] }, _then: 255, }, }; const data = { mode: "B", bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); it("uses has op", () => { const bp = createBitPacker(); const schema = { tags: [3, "-8"], bonus: { _when: { field: "tags", op: "has", value: "vip" }, _then: 255, }, }; const data = { tags: ["vip", "pro"], bonus: 10 }; expect(bp.unpack(bp.pack(data, schema), schema)).toEqual(data); }); }); describe("conditional direct", () => { it("isConditionalSchema returns false for invalid predicates", () => { expect( isConditionalSchema({ _if: { field: "", op: "==", value: 1 }, _then: 255, }) ).toBe(false); expect( isConditionalSchema({ _if: { field: "x", op: "??", value: 1 }, _then: 255, }) ).toBe(false); expect( isConditionalSchema({ _if: { field: "x", op: "==" }, _then: 255, }) ).toBe(false); expect(isConditionalSchema({ _then: 255 })).toBe(false); expect(isConditionalSchema("bad")).toBe(false); }); it("evaluatePredicate handles all ops", () => { expect(evaluatePredicate({ x: 5 }, "x", "==", 5)).toBe(true); expect(evaluatePredicate({ x: 5 }, "x", "!=", 5)).toBe(false); expect(evaluatePredicate({ x: 5 }, "x", "<", 10)).toBe(true); expect(evaluatePredicate({ x: 5 }, "x", "<=", 5)).toBe(true); expect(evaluatePredicate({ x: 5 }, "x", ">", 1)).toBe(true); expect(evaluatePredicate({ x: 5 }, "x", ">=", 5)).toBe(true); expect(evaluatePredicate({ x: "a" }, "x", "in", ["a", "b"])).toBe( true ); expect(evaluatePredicate({ x: ["a", "b"] }, "x", "has", "a")).toBe( true ); }); it("evaluatePredicate handles null context", () => { expect(evaluatePredicate(null, "x", "==", 1)).toBe(false); }); it("evaluatePredicate handles non-array in", () => { expect(evaluatePredicate({ x: "a" }, "x", "in", "not-array")).toBe( false ); }); it("evaluatePredicate handles non-array has", () => { expect(evaluatePredicate({ x: "a" }, "x", "has", "a")).toBe(false); }); it("resolveBranch returns NO_BRANCH when predicate missing", () => { const strategy = new ConditionalPackingStrategy(); const result = (strategy as any).resolveBranch( { _then: 255 }, {} as unknown as PackingContext, {} ); expect(result).toBeDefined(); // NO_BRANCH is a unique symbol, so we just check it's not a schema expect(typeof result).toBe("symbol"); }); it("resolveBranch uses data when parentData is undefined", () => { const strategy = new ConditionalPackingStrategy(); const result = (strategy as any).resolveBranch( { _if: { field: "x", op: "==", value: 1 }, _then: 255, }, {} as unknown as PackingContext, { x: 1 } ); expect(result).toBe(255); }); }); describe("union", () => { it("round-trips discriminated union", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _union: { on: "kind", cases: { move: { kind: "-8", x: 255, y: 255 }, chat: { kind: "-8", text: "-16" }, }, }, }; const moveData = { kind: "move", x: 10, y: 20 }; const chatData = { kind: "chat", text: "hello" }; expect(bp.unpack(bp.pack(moveData, schema), schema)).toEqual( moveData ); expect(bp.unpack(bp.pack(chatData, schema), schema)).toEqual( chatData ); }); it("throws for invalid discriminator", () => { const bp = createBitPacker(); const schema: BitPackerSchema = { _union: { on: "kind", cases: { move: { kind: "-8", x: 255 }, }, }, }; expect(() => bp.pack({ kind: "chat" }, schema)).toThrow(); }); it("throws on unpack when case index out of range", () => { const strategy = new UnionPackingStrategy(); const reader = { readUint: () => 5 }; const context = { reader, unpackValue: () => {}, } as unknown as PackingContext; const schema = { _union: { on: "kind", cases: { a: { kind: "-8" } }, }, }; expect(() => strategy.unpack(schema, context)).toThrow(); }); it("isUnionSchema rejects non-objects", async () => { const { isUnionSchema } = await import( "../strategies/union/union.js" ); expect(isUnionSchema("bad")).toBe(false); }); it("isUnionSchema rejects invalid cases", async () => { const { isUnionSchema } = await import( "../strategies/union/union.js" ); expect(isUnionSchema({ _union: { on: "kind" } })).toBe(false); expect(isUnionSchema({ _union: { on: "kind", cases: [] } })).toBe( false ); expect(isUnionSchema({ _union: { on: "kind", cases: {} } })).toBe( false ); }); it("throws when union case schema is undefined", () => { const bp = createBitPacker(); const schema = { _union: { on: "kind", cases: { a: undefined }, }, } as unknown as BitPackerSchema; expect(() => bp.pack({ kind: "a" }, schema)).toThrow(); }); it("throws on unpack when union case schema is undefined", () => { const strategy = new UnionPackingStrategy(); const reader = { readUint: () => 0 }; const context = { reader, unpackValue: () => {}, } as unknown as PackingContext; const schema = { _union: { on: "kind", cases: { a: undefined }, }, }; expect(() => strategy.unpack(schema, context)).toThrow(); }); it("isUnionSchema rejects non-string on", async () => { const { isUnionSchema } = await import( "../strategies/union/union.js" ); expect( isUnionSchema({ _union: { on: 123, cases: { a: 255 } } }) ).toBe(false); }); it("throws for non-object data in union", () => { const strategy = new UnionPackingStrategy(); const schema = { _union: { on: "kind", cases: { a: 255 } }, } as unknown as BitPackerSchema; expect(() => strategy.pack("bad", schema, {} as unknown as PackingContext) ).toThrow(); }); }); describe("complex nested", () => { it("round-trips game state-like object", () => { const bp = createBitPacker(); const schema = { id: "uuid", health: 255, pos: { x: 1023, y: 1023 }, tags: [4, "-8"], weapon: { _optional: { name: "-16", damage: 255, }, }, }; const data = { id: "550e8400-e29b-41d4-a716-446655440000", health: 100, pos: { x: 500, y: 250 }, tags: ["fast", "rare"], weapon: { name: "sword", damage: 50, }, }; const packed = bp.pack(data, schema); const unpacked = bp.unpack(packed, schema); expect(unpacked).toEqual(data); }); }); describe("JSON purity", () => { it("round-trips every example schema through JSON.stringify/parse", () => { const schemas: BitPackerSchema[] = [ 255, -127, "8", "-16", "uuid", "f32", "f64", true, null, "bigint-i64", "bigint-u64", { x: 255, y: "-16", flag: true }, [255], [10, 255], { _enum: ["red", "green", "blue"] }, { _optional: 255 }, { _oneOf: ["-8", 255] }, { _if: { field: "active", op: "==", value: true }, _then: { level: 255 }, }, { _when: { field: "active", op: "==", value: true }, _then: { level: 255 }, }, { _customType: "date" }, { _union: { on: "kind", cases: { move: { kind: "-8", x: 255 }, chat: { kind: "-8", text: "-16" }, }, }, }, { _blob: { maxLength: 100 } }, { _map: { key: "-8", value: 255, maxSize: 10 } }, { _set: { element: 255, maxSize: 10 } }, { _ref: "player" }, ]; for (const schema of schemas) { const reparsed = JSON.parse(JSON.stringify(schema)); expect(reparsed).toEqual(schema); } }); }); describe("_customType", () => { it("round-trips via CustomTypeRegistry", () => { const handler: CustomTypeHandler = { typeName: "test-int", pack(value) { const buf = new ArrayBuffer(4); new DataView(buf).setInt32(0, value, true); return new Uint8Array(buf); }, unpack(data) { return new DataView( data.buffer, data.byteOffset, data.byteLength ).getInt32(0, true); }, validate(value): boolean { return typeof value === "number"; }, getSchema() { return { _customType: "test-int" }; }, getSize() { return 4; }, }; CustomTypeRegistry.getInstance().register(handler); const bp = createBitPacker(); const schema: BitPackerSchema = { _customType: "test-int" }; const packed = bp.pack(12345, schema); expect(bp.unpack(packed, schema)).toBe(12345); }); it("round-trips via _bp wrapper", () => { const bp = createBitPacker(); const schema = { _bp: { schema: 255 } }; expect(bp.unpack(bp.pack(42, schema), schema)).toBe(42); }); it("throws for unregistered custom type on pack", () => { const bp = createBitPacker(); const schema = { _customType: "definitely-missing-handler" }; expect(() => bp.pack(42, schema)).toThrow(); }); it("throws for unregistered custom type on unpack", () => { const bp = createBitPacker(); const schema = { _customType: "definitely-missing-handler-2" }; // We need some packed data to unpack; use a valid pack then swap schema const packed = bp.pack(42, 255); expect(() => bp.unpack(packed, schema)).toThrow(); }); }); });