150 lines
5.0 KiB
TypeScript
150 lines
5.0 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import fc from "fast-check";
|
|
|
|
import {
|
|
BITSET_HEADER_SIZE,
|
|
chunkIdsToRawBitset,
|
|
decodeBitset as decodeFramed,
|
|
encodeBitset as encodeFramed,
|
|
rawBitsetToChunkIds,
|
|
} from "../src/codec/bitset.js";
|
|
import { DMP_CODES, DmpError } from "../src/error-codes.js";
|
|
import {
|
|
decodeBitsetToChunkIds as decodeBitset,
|
|
encodeBitsetFromChunkIds as encodeBitset,
|
|
SELECT_FORM_BITSET_THRESHOLD,
|
|
selectForm,
|
|
selectWhole,
|
|
} from "../src/select/form.js";
|
|
|
|
describe("@suaveplan/drive-mesh-protocol — selectForm", () => {
|
|
it("rejects empty needed list with EMPTY_REQUEST", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(() => selectForm([])).toThrow(DmpError);
|
|
try {
|
|
selectForm([]);
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.EMPTY_REQUEST);
|
|
}
|
|
});
|
|
|
|
it("returns kind:'specific' for ≤ threshold counts", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const result = selectForm([0, 5, 17]);
|
|
expect(result.kind).toBe("specific");
|
|
if (result.kind === "specific") {
|
|
expect(result.chunkIds).toEqual([0, 5, 17]);
|
|
}
|
|
});
|
|
|
|
it("returns kind:'specific' at exactly threshold", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const ids = Array.from(
|
|
{ length: SELECT_FORM_BITSET_THRESHOLD },
|
|
(_, i) => i
|
|
);
|
|
const result = selectForm(ids);
|
|
expect(result.kind).toBe("specific");
|
|
});
|
|
|
|
it("returns kind:'bitset' for > threshold counts", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const ids = Array.from(
|
|
{ length: SELECT_FORM_BITSET_THRESHOLD + 1 },
|
|
(_, i) => i
|
|
);
|
|
const result = selectForm(ids);
|
|
expect(result.kind).toBe("bitset");
|
|
if (result.kind === "bitset") {
|
|
expect(result.bytes).toBeInstanceOf(Uint8Array);
|
|
expect(result.bytes.length).toBeGreaterThan(BITSET_HEADER_SIZE);
|
|
}
|
|
});
|
|
|
|
it("honors a caller-supplied threshold override", { timeout: 5000 }, () => {
|
|
const result = selectForm([0, 1, 2], { threshold: 2 });
|
|
expect(result.kind).toBe("bitset");
|
|
});
|
|
|
|
it("threshold is exposed as the documented 64", { timeout: 5000 }, () => {
|
|
expect(SELECT_FORM_BITSET_THRESHOLD).toBe(64);
|
|
});
|
|
});
|
|
|
|
describe("@suaveplan/drive-mesh-protocol — selectWhole", () => {
|
|
it("returns the canonical whole form", { timeout: 5000 }, () => {
|
|
expect(selectWhole()).toEqual({ kind: "whole" });
|
|
});
|
|
});
|
|
|
|
describe("@suaveplan/drive-mesh-protocol — chunkIdsToRawBitset / rawBitsetToChunkIds", () => {
|
|
it("encodes a single bit", { timeout: 5000 }, () => {
|
|
const raw = chunkIdsToRawBitset([0], 1);
|
|
expect(raw.length).toBe(1);
|
|
expect(raw[0]).toBe(0b00000001);
|
|
});
|
|
|
|
it("encodes bits across byte boundaries", { timeout: 5000 }, () => {
|
|
const raw = chunkIdsToRawBitset([0, 7, 8, 15], 16);
|
|
expect(raw.length).toBe(2);
|
|
expect(raw[0]).toBe(0b10000001);
|
|
expect(raw[1]).toBe(0b10000001);
|
|
});
|
|
|
|
it("rejects negative chunk ids", { timeout: 5000 }, () => {
|
|
expect(() => chunkIdsToRawBitset([-1], 8)).toThrow(DmpError);
|
|
});
|
|
|
|
it("rejects ids past bitLength", { timeout: 5000 }, () => {
|
|
expect(() => chunkIdsToRawBitset([10], 8)).toThrow(DmpError);
|
|
});
|
|
|
|
it("decodes returns indexes in ascending order", { timeout: 5000 }, () => {
|
|
const raw = chunkIdsToRawBitset([10, 3, 17, 0], 18);
|
|
expect(rawBitsetToChunkIds(raw, 18)).toEqual([0, 3, 10, 17]);
|
|
});
|
|
});
|
|
|
|
describe("@suaveplan/drive-mesh-protocol — encodeBitset / decodeBitset (framed)", () => {
|
|
it("framed bytes carry the magic prefix", { timeout: 5000 }, () => {
|
|
const bytes = encodeBitset([0, 5, 17]);
|
|
expect(bytes[0]).toBe(0x44); // 'D'
|
|
expect(bytes[1]).toBe(0x4d); // 'M'
|
|
expect(bytes[2]).toBe(0x50); // 'P'
|
|
expect(bytes[3]).toBe(0x42); // 'B'
|
|
});
|
|
|
|
it("decode honours the embedded bit-length", { timeout: 5000 }, () => {
|
|
const framed = encodeFramed(chunkIdsToRawBitset([0, 5, 17], 18), 18);
|
|
const round = decodeFramed(framed);
|
|
expect(round.bitLength).toBe(18);
|
|
});
|
|
|
|
it("roundtrip property: decode(encode(x)) preserves the sorted set", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc
|
|
.array(fc.integer({ min: 0, max: 1023 }), {
|
|
minLength: 1,
|
|
maxLength: 64,
|
|
})
|
|
.map((arr) => Array.from(new Set(arr))),
|
|
(input) => {
|
|
const sorted = [...input].sort((a, b) => a - b);
|
|
const framed = encodeBitset(sorted);
|
|
const recovered = decodeBitset(framed);
|
|
expect(recovered).toEqual(sorted);
|
|
}
|
|
),
|
|
{ numRuns: 50 }
|
|
);
|
|
});
|
|
});
|