504 lines
16 KiB
TypeScript
504 lines
16 KiB
TypeScript
/**
|
|
* Edge-case coverage for the codec / trust / generation modules:
|
|
* malformed JSON, schema mismatches, popcount, scoring math, and the
|
|
* branches that the spec-tied tests didn't exercise directly.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
AdvertiseThrottle,
|
|
chunkIdsToRawBitset,
|
|
DMP_CODES,
|
|
DmpError,
|
|
decodeAdvertisePayload,
|
|
decodeErrorPayload,
|
|
decodeRequestPayload,
|
|
decodeResponsePayload,
|
|
encodeAdvertisePayload,
|
|
encodeBitsetFromChunkIds,
|
|
encodeErrorPayload,
|
|
encodeFramedBitset,
|
|
encodeRequestPayload,
|
|
encodeResponsePayload,
|
|
GenerationTracker,
|
|
popcount,
|
|
rawBitsetToChunkIds,
|
|
selectForm,
|
|
shouldSkip,
|
|
type TrustEntry,
|
|
TrustMap,
|
|
} from "../src/index.js";
|
|
|
|
const FILE_ID = "e".repeat(64);
|
|
|
|
describe("popcount", () => {
|
|
it("counts zero", { timeout: 5000 }, () => {
|
|
expect(popcount(new Uint8Array([0, 0, 0]))).toBe(0);
|
|
});
|
|
it("counts mixed", { timeout: 5000 }, () => {
|
|
expect(popcount(new Uint8Array([0xff, 0x01, 0x80]))).toBe(8 + 1 + 1);
|
|
});
|
|
});
|
|
|
|
describe("chunkIdsToRawBitset edge cases", () => {
|
|
it("rejects non-integer bitLength", { timeout: 5000 }, () => {
|
|
try {
|
|
chunkIdsToRawBitset([0], 1.5);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(
|
|
DMP_CODES.BITSET_LENGTH_MISMATCH
|
|
);
|
|
}
|
|
});
|
|
it("rejects negative bitLength", { timeout: 5000 }, () => {
|
|
expect(() => chunkIdsToRawBitset([], -1)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("rawBitsetToChunkIds edge cases", () => {
|
|
it("rejects non-zero padding bits", { timeout: 5000 }, () => {
|
|
const raw = new Uint8Array([0xff]);
|
|
try {
|
|
rawBitsetToChunkIds(raw, 4);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(
|
|
DMP_CODES.BITSET_PADDING_NONZERO
|
|
);
|
|
}
|
|
});
|
|
it("rejects byte-length / bit-length mismatch", { timeout: 5000 }, () => {
|
|
const raw = new Uint8Array([0]);
|
|
try {
|
|
rawBitsetToChunkIds(raw, 16);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(
|
|
DMP_CODES.BITSET_LENGTH_MISMATCH
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("encodeFramedBitset rejects bad inputs", () => {
|
|
it("rejects non-zero padding at encode time", { timeout: 5000 }, () => {
|
|
const raw = new Uint8Array([0xff]);
|
|
try {
|
|
encodeFramedBitset(raw, 4);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(
|
|
DMP_CODES.BITSET_PADDING_NONZERO
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Codec malformed JSON paths", () => {
|
|
it("decodeRequestPayload rejects malformed JSON", { timeout: 5000 }, () => {
|
|
try {
|
|
decodeRequestPayload(new TextEncoder().encode("not json"));
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
it("decodeAdvertisePayload rejects malformed JSON", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
decodeAdvertisePayload(new TextEncoder().encode("not json"));
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
it("decodeResponsePayload rejects malformed JSON", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
decodeResponsePayload(new TextEncoder().encode("not json"));
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Codec unwrapWire passthrough on non-object", () => {
|
|
it("decodeAdvertisePayload rejects bare null", { timeout: 5000 }, () => {
|
|
const wire = new TextEncoder().encode(JSON.stringify(null));
|
|
try {
|
|
decodeAdvertisePayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
it("decodeRequestPayload rejects array", { timeout: 5000 }, () => {
|
|
const wire = new TextEncoder().encode(JSON.stringify([1, 2, 3]));
|
|
try {
|
|
decodeRequestPayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
it("decodeResponsePayload rejects bare object", { timeout: 5000 }, () => {
|
|
const wire = new TextEncoder().encode(JSON.stringify({ x: 1 }));
|
|
try {
|
|
decodeResponsePayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Codec encode-time validation", () => {
|
|
it("encodeRequestPayload rejects malformed input", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
try {
|
|
encodeRequestPayload({
|
|
fileId: "not-hex",
|
|
request: { kind: "whole" },
|
|
priority: "normal",
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
|
|
it("encodeResponsePayload rejects bad presence", { timeout: 5000 }, () => {
|
|
try {
|
|
encodeResponsePayload({
|
|
fileId: FILE_ID,
|
|
chunks: [],
|
|
presence: new Uint8Array(2).fill(0xff), // missing magic
|
|
generation: 1,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
|
|
it("encodeAdvertisePayload rejects bad presence", { timeout: 5000 }, () => {
|
|
try {
|
|
encodeAdvertisePayload({
|
|
fileId: FILE_ID,
|
|
presence: new Uint8Array(20).fill(0xff),
|
|
totalChunks: 8,
|
|
generation: 1,
|
|
timestamp: 0,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
|
|
it("encodeErrorPayload rejects malformed input", { timeout: 5000 }, () => {
|
|
try {
|
|
encodeErrorPayload({
|
|
fileId: "non-hex-bad-id!",
|
|
correlationId: "x",
|
|
code: "INTERNAL",
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.ENVELOPE_MALFORMED);
|
|
}
|
|
});
|
|
|
|
it("decodeErrorPayload accepts a clean envelope", { timeout: 5000 }, () => {
|
|
const wire = encodeErrorPayload({
|
|
fileId: FILE_ID,
|
|
correlationId: "abc",
|
|
code: "INTERNAL",
|
|
});
|
|
const back = decodeErrorPayload(wire);
|
|
expect(back.code).toBe("INTERNAL");
|
|
});
|
|
|
|
it("encodeRequest rejects bitset without magic", { timeout: 5000 }, () => {
|
|
try {
|
|
encodeRequestPayload({
|
|
fileId: FILE_ID,
|
|
request: {
|
|
kind: "bitset",
|
|
bytes: new Uint8Array(4).fill(0),
|
|
},
|
|
priority: "normal",
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.CODEC_MISMATCH);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("encodeBitsetFromChunkIds (high-level helper)", () => {
|
|
it("frames a chunk-id list via the canonical pipeline", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const framed = encodeBitsetFromChunkIds([3, 7], 16);
|
|
expect(framed.length).toBeGreaterThan(0);
|
|
});
|
|
it("derives bitLength from max id when not supplied", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const framed = encodeBitsetFromChunkIds([10]);
|
|
expect(framed.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("selectForm bitLength override", () => {
|
|
it("uses caller-supplied bitLength when present", { timeout: 5000 }, () => {
|
|
const ids = Array.from({ length: 80 }, (_, i) => i);
|
|
const r = selectForm(ids, { bitLength: 200 });
|
|
expect(r.kind).toBe("bitset");
|
|
});
|
|
});
|
|
|
|
describe("TrustMap arithmetic", () => {
|
|
it("score reflects miss / advertised ratio", { timeout: 5000 }, () => {
|
|
const t = new TrustMap();
|
|
t.recordAdvertised("p", "f", 10);
|
|
t.recordMissing("p", "f", 1);
|
|
const e = t.get("p", "f");
|
|
expect(e.score).toBeCloseTo(0.9, 4);
|
|
});
|
|
});
|
|
|
|
describe("decodeAdvertisePayload bitset length cross-check", () => {
|
|
it("rejects wire where presence bitLength ≠ totalChunks", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const presence = encodeFramedBitset(chunkIdsToRawBitset([0, 1], 8), 8);
|
|
// Hand-build a wire envelope where totalChunks (16) doesn't
|
|
// match the bitset's embedded bitLength (8).
|
|
const wire = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
fileId: FILE_ID,
|
|
presence: { _b: btoa(String.fromCharCode(...presence)) },
|
|
totalChunks: 16,
|
|
generation: 1,
|
|
timestamp: 0,
|
|
})
|
|
);
|
|
try {
|
|
decodeAdvertisePayload(wire);
|
|
throw new Error("expected mismatch");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(
|
|
DMP_CODES.BITSET_LENGTH_MISMATCH
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("DMP-01 — unknown kind rejection", () => {
|
|
it("decodeRequestPayload rejects unknown kind with UNKNOWN_REQUEST_KIND", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const wire = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
fileId: FILE_ID,
|
|
request: { kind: "unknown" },
|
|
priority: "normal",
|
|
})
|
|
);
|
|
try {
|
|
decodeRequestPayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.UNKNOWN_REQUEST_KIND);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Tiny coverage gaps", () => {
|
|
it("encodeRequest direct call rejects empty specific", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const wire = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
fileId: FILE_ID,
|
|
request: { kind: "specific", chunkIds: [] },
|
|
priority: "normal",
|
|
})
|
|
);
|
|
try {
|
|
decodeRequestPayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.EMPTY_REQUEST);
|
|
}
|
|
});
|
|
|
|
it("encodeRequest function exposed and rejects empty specific", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { encodeRequest } = await import("../src/index.js");
|
|
try {
|
|
encodeRequest({ kind: "specific", chunkIds: [] });
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DmpError).code).toBe(DMP_CODES.EMPTY_REQUEST);
|
|
}
|
|
});
|
|
|
|
it("empty bitset (bitLength 0) round-trips cleanly", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const raw = new Uint8Array(0);
|
|
const framed = encodeFramedBitset(raw, 0);
|
|
const decoded = encodeFramedBitset(raw, 0);
|
|
expect(decoded).toEqual(framed);
|
|
// popcount of an empty array
|
|
expect(popcount(raw)).toBe(0);
|
|
});
|
|
|
|
it("selectForm with non-monotonic ids covers branch", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const r = selectForm([5, 3, 7, 1], { threshold: 2 });
|
|
expect(r.kind).toBe("bitset");
|
|
});
|
|
|
|
it("encodeBitsetFromChunkIds with non-monotonic ids", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const framed = encodeBitsetFromChunkIds([10, 3, 7]);
|
|
expect(framed.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("TrustMap `size` getter is exposed", { timeout: 5000 }, () => {
|
|
const t = new TrustMap();
|
|
t.recordAdvertised("a", "b", 1);
|
|
expect(t.size).toBe(1);
|
|
});
|
|
|
|
it("GenerationTracker `size` getter is exposed", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const g = new GenerationTracker();
|
|
g.observe("p", "f", 1);
|
|
expect(g.size).toBe(1);
|
|
});
|
|
|
|
it("AdvertiseThrottle `windowMs` getter is exposed", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new AdvertiseThrottle({ clock: () => 0, windowMs: 1234 });
|
|
expect(t.windowMs).toBe(1234);
|
|
});
|
|
|
|
it("Throttle.hasPending returns false for unknown key", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new AdvertiseThrottle({ clock: () => 0, windowMs: 1000 });
|
|
expect(t.hasPending("never-seen", "f")).toBe(false);
|
|
});
|
|
|
|
it("Throttle.clearPending no-ops for unknown key", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new AdvertiseThrottle({ clock: () => 0, windowMs: 1000 });
|
|
// Should not throw.
|
|
t.clearPending("never-seen", "f");
|
|
expect(t.size).toBe(0);
|
|
});
|
|
|
|
it("Throttle.dropNeighbor uses default windowMs when none supplied", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new AdvertiseThrottle({ clock: () => 0 });
|
|
expect(t.windowMs).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("computeScore returns 1 when advertised === 0 (defence-in-depth)", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new TrustMap();
|
|
// Record a missing without any prior advertise.
|
|
t.recordMissing("ghost", "f", 5);
|
|
expect(t.get("ghost", "f").score).toBe(1);
|
|
});
|
|
|
|
it("probabilityOfSkip returns 0 for advertised === 0", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const { probabilityOfSkip } = await import("../src/index.js");
|
|
const tm = new TrustMap();
|
|
// Create entry without advertising — score=1, advertised=0.
|
|
const e = tm.get("p", "f");
|
|
expect(probabilityOfSkip(e)).toBe(0);
|
|
});
|
|
|
|
it("shouldSkip: probability strictly between 0 and 1 hits rng path", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new TrustMap();
|
|
t.recordAdvertised("p", "f", 100);
|
|
t.recordMissing("p", "f", 6); // 0.06 miss rate; p = 0.2
|
|
const e = t.get("p", "f");
|
|
// shouldSkip falls through to rng.nextFloat() < 0.2 — both
|
|
// branches matter for coverage.
|
|
const draws = [0.1, 0.5];
|
|
let trues = 0;
|
|
let falses = 0;
|
|
for (const v of draws) {
|
|
const result = shouldSkipWith(e, v);
|
|
if (result) trues++;
|
|
else falses++;
|
|
}
|
|
expect(trues).toBe(1);
|
|
expect(falses).toBe(1);
|
|
});
|
|
|
|
it("score formula for advertised > 0 returns the ratio", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const t = new TrustMap();
|
|
t.recordAdvertised("p", "f", 4);
|
|
t.recordMissing("p", "f", 1);
|
|
expect(t.get("p", "f").score).toBeCloseTo(0.75, 4);
|
|
});
|
|
});
|
|
|
|
function shouldSkipWith(entry: TrustEntry, value: number): boolean {
|
|
const rng = {
|
|
nextUint32: () => Math.floor(value * 2 ** 32),
|
|
nextFloat: () => value,
|
|
bytes: (n: number) => new Uint8Array(n),
|
|
};
|
|
return shouldSkip(entry, rng);
|
|
}
|
|
|
|
describe("Generation negative path", () => {
|
|
it("generation past u32 max rejected", { timeout: 5000 }, () => {
|
|
const wire = new TextEncoder().encode(
|
|
JSON.stringify({
|
|
fileId: FILE_ID,
|
|
presence: {
|
|
_b: btoa(String.fromCharCode(...new Uint8Array(0))),
|
|
},
|
|
totalChunks: 0,
|
|
generation: 0x1_00_00_00_00,
|
|
timestamp: 0,
|
|
})
|
|
);
|
|
try {
|
|
decodeAdvertisePayload(wire);
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
}
|
|
});
|
|
});
|