/** * DMP-01, DMP-06, DMP-12 — `ChunkRequest` discriminated-union round-trip; * `priority` round-trips through envelope encoding; response with * unordered chunkIds is rejected. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import { chunkIdsToRawBitset, DMP_CODES, DmpError, decodeRequestPayload, decodeResponsePayload, encodeFramedBitset, encodeRequestPayload, encodeResponsePayload, selectWhole, } from "../src/index.js"; import type { ChunkRequestPayload, ChunkResponsePayload, } from "../src/types/protocol.types.js"; const FILE_ID = "a".repeat(64); function roundTrip(payload: ChunkRequestPayload): ChunkRequestPayload { return decodeRequestPayload(encodeRequestPayload(payload)); } describe("DMP-01 — ChunkRequest discriminated union", () => { it("every ChunkRequest form roundtrips through encode/decode", { timeout: 5000, }, () => { const whole = roundTrip({ fileId: FILE_ID, request: selectWhole(), priority: "normal", }); expect(whole.request.kind).toBe("whole"); const specific = roundTrip({ fileId: FILE_ID, request: { kind: "specific", chunkIds: [0, 5, 17] }, priority: "normal", }); expect(specific.request.kind).toBe("specific"); if (specific.request.kind === "specific") { expect(specific.request.chunkIds).toEqual([0, 5, 17]); } const framed = encodeFramedBitset(chunkIdsToRawBitset([3, 7], 16), 16); const bitset = roundTrip({ fileId: FILE_ID, request: { kind: "bitset", bytes: framed }, priority: "high", }); expect(bitset.request.kind).toBe("bitset"); if (bitset.request.kind === "bitset") { expect(bitset.request.bytes).toEqual(framed); } }); it("exhaustive switch over the union compiles to no never-narrowing", { timeout: 5000, }, () => { const cover = (req: ChunkRequestPayload["request"]): string => { switch (req.kind) { case "whole": return "whole"; case "specific": return `specific:${req.chunkIds.length}`; case "bitset": return `bitset:${req.bytes.length}`; } }; expect(cover({ kind: "whole" })).toBe("whole"); }); }); describe("DMP-06 — priority round-trip", () => { it("priority field round-trips through envelope encoding", { timeout: 5000, }, () => { for (const priority of [ "critical", "high", "normal", "low", "bulk", ] as const) { const round = roundTrip({ fileId: FILE_ID, request: selectWhole(), priority, }); expect(round.priority).toBe(priority); } }); }); describe("DMP-12 — response chunks ordered ascending", () => { it("response with unordered chunkIds rejected", { timeout: 5000 }, () => { const presence = encodeFramedBitset(chunkIdsToRawBitset([0, 1], 8), 8); const payload: ChunkResponsePayload = { fileId: FILE_ID, chunks: [ { chunkId: 1, bytes: new Uint8Array([1]) }, { chunkId: 0, bytes: new Uint8Array([2]) }, ], presence, generation: 1, }; try { encodeResponsePayload(payload); throw new Error("expected unordered rejection"); } catch (err) { expect(err).toBeInstanceOf(DmpError); expect((err as DmpError).code).toBe(DMP_CODES.UNORDERED_RESPONSE); } }); it("decoder also rejects unordered chunks (defence-in-depth)", { timeout: 5000, }, () => { // Build an ordered envelope, then mutate the JSON to swap. const presence = encodeFramedBitset( chunkIdsToRawBitset([0, 1, 2], 8), 8 ); const ok: ChunkResponsePayload = { fileId: FILE_ID, chunks: [ { chunkId: 0, bytes: new Uint8Array([10]) }, { chunkId: 1, bytes: new Uint8Array([20]) }, ], presence, generation: 5, }; const wire = encodeResponsePayload(ok); const text = new TextDecoder().decode(wire); const obj = JSON.parse(text); // Swap the chunkIds. const tmp = obj.chunks[0].chunkId; obj.chunks[0].chunkId = obj.chunks[1].chunkId; obj.chunks[1].chunkId = tmp; const tampered = new TextEncoder().encode(JSON.stringify(obj)); try { decodeResponsePayload(tampered); throw new Error("expected unordered rejection"); } catch (err) { expect(err).toBeInstanceOf(DmpError); expect((err as DmpError).code).toBe(DMP_CODES.UNORDERED_RESPONSE); } }); });