107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
/**
|
|
* DMP-05 / DMP-21 — selectForm threshold; codec-mandate enforcement.
|
|
*
|
|
* `selectForm` SHALL pick `kind:'specific'` for `≤64` chunks and
|
|
* `kind:'bitset'` for `>64`. Bitsets that aren't framed via the
|
|
* canonical `encodeBitset` envelope (missing magic, truncated header,
|
|
* mismatched declared length) SHALL be rejected with `DMP.CODEC_MISMATCH`.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
BITSET_HEADER_SIZE,
|
|
BITSET_MAGIC,
|
|
chunkIdsToRawBitset,
|
|
DMP_CODES,
|
|
DmpError,
|
|
decodeFramedBitset,
|
|
decodeRequestPayload,
|
|
encodeFramedBitset,
|
|
SELECT_FORM_BITSET_THRESHOLD,
|
|
selectForm,
|
|
} from "../src/index.js";
|
|
|
|
const FILE_ID = "0".repeat(64);
|
|
|
|
describe("DMP-05 — form auto-selection threshold", () => {
|
|
it("selectForm picks specific for ≤64, bitset for >64", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
for (let n = 1; n <= SELECT_FORM_BITSET_THRESHOLD; n++) {
|
|
const ids = Array.from({ length: n }, (_, i) => i);
|
|
const r = selectForm(ids);
|
|
expect(r.kind).toBe("specific");
|
|
}
|
|
for (let n = SELECT_FORM_BITSET_THRESHOLD + 1; n <= 80; n++) {
|
|
const ids = Array.from({ length: n }, (_, i) => i);
|
|
const r = selectForm(ids);
|
|
expect(r.kind).toBe("bitset");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("DMP-21 — codec mandates", () => {
|
|
it("non-bitpacker-compressed bitset rejected (missing magic)", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const fakeBitset = new Uint8Array(20).fill(0xff);
|
|
// Wire payload synthesised manually with a non-magic byte.
|
|
const wire = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
fileId: FILE_ID,
|
|
request: {
|
|
kind: "bitset",
|
|
bytes: { _b: btoa(String.fromCharCode(...fakeBitset)) },
|
|
},
|
|
priority: "normal",
|
|
})
|
|
);
|
|
try {
|
|
decodeRequestPayload(wire);
|
|
throw new Error("expected codec mismatch");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
|
|
it("truncated bitset header rejected with CODEC_MISMATCH", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const tooShort = new Uint8Array(BITSET_HEADER_SIZE - 2);
|
|
try {
|
|
decodeFramedBitset(tooShort);
|
|
throw new Error("expected truncation rejection");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
|
|
it("valid bitset with magic decodes successfully", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const raw = chunkIdsToRawBitset([0, 5, 17], 18);
|
|
const framed = encodeFramedBitset(raw, 18);
|
|
for (let i = 0; i < BITSET_MAGIC.length; i++) {
|
|
expect(framed[i]).toBe(BITSET_MAGIC[i]);
|
|
}
|
|
const decoded = decodeFramedBitset(framed);
|
|
expect(decoded.bitLength).toBe(18);
|
|
});
|
|
|
|
it("framed length mismatch rejected", { timeout: 5000 }, () => {
|
|
const raw = chunkIdsToRawBitset([0, 1], 8);
|
|
const framed = encodeFramedBitset(raw, 8);
|
|
// Truncate the body so rawByteLen no longer matches.
|
|
const broken = framed.slice(0, framed.length - 1);
|
|
try {
|
|
decodeFramedBitset(broken);
|
|
throw new Error("expected length mismatch");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
});
|