1import { describe, expect, it } from "vitest";2import {3 padSyncFrame,4 syncFrameBucket,5 unpadSyncFrame,6 MAX_NOISE_PLAINTEXT_BYTES,7 FRAME_CONTINUATION_MARKER,8 chunkPaddedSyncFrame,9} from "./sync-padding";1011describe("sync frame padding (E7)", () => {12 it("round-trips payloads of every shape, including empty", () => {13 for (const size of [0, 1, 5, 251, 252, 253, 1000, 70_000]) {14 const payload = Uint8Array.from({ length: size }, (_, i) => (i * 31) & 0xff);15 expect(Array.from(unpadSyncFrame(padSyncFrame(payload)))).toEqual(16 Array.from(payload),17 );18 }19 });2021 it("collapses every small control frame to one indistinguishable size", () => {22 23 24 const hello = new TextEncoder().encode('{"type":"SYNC_HELLO","vaultId":"' + "a".repeat(64) + '"}');25 const done = new TextEncoder().encode('{"type":"SYNC_DONE"}');26 expect(padSyncFrame(hello).length).toBe(padSyncFrame(done).length);27 expect(padSyncFrame(done).length).toBe(256);28 });2930 it("pins the bucket ladder — cross-platform KAT, must match peach-android", () => {31 const cases: Array<[number, number]> = [32 [0, 256],33 [252, 256],34 [253, 512],35 [1024, 2048],36 [65_532, 65_536],37 [65_533, 131_072],38 [200_000, 262_144],39 ];40 for (const [payload, expected] of cases) {41 expect(syncFrameBucket(4 + payload)).toBe(expected);42 expect(padSyncFrame(new Uint8Array(payload)).length).toBe(expected);43 }44 });4546 it("keeps large-frame overhead bounded rather than doubling", () => {47 48 49 const twelveMiB = 12 * 1024 * 1024;50 const padded = syncFrameBucket(twelveMiB);51 expect(padded).toBeGreaterThanOrEqual(twelveMiB);52 expect(padded - twelveMiB).toBeLessThan(64 * 1024);53 });5455 it("rejects a frame whose declared length exceeds what it carries", () => {56 const forged = new Uint8Array(256);57 new DataView(forged.buffer).setUint32(0, 9999, false);58 expect(() => unpadSyncFrame(forged)).toThrow(/more payload than it carries/);59 });6061 it("rejects a frame shorter than its length header", () => {62 expect(() => unpadSyncFrame(new Uint8Array(3))).toThrow(/length header/);63 });64});656667686970717273describe("sync frame chunking (E7b)", () => {74 it("MAX_NOISE_PLAINTEXT_BYTES matches what the crypto core allows", () => {75 76 77 78 expect(MAX_NOISE_PLAINTEXT_BYTES).toBe(65519);79 });8081 it("the bucket ladder can produce frames above the Noise limit — the bug", () => {82 83 84 85 86 87 88 const payload = Uint8Array.from({ length: 32 * 1024 }, () => 0);89 const padded = padSyncFrame(payload);90 expect(padded.byteLength).toBeGreaterThan(MAX_NOISE_PLAINTEXT_BYTES);91 });9293 it("the continuation marker is the byte the chunking protocol uses", () => {94 95 96 97 expect(FRAME_CONTINUATION_MARKER).toBe(0x01);98 });99100 it("production chunking reserves marker space at the Noise boundary", () => {101 const payload = Uint8Array.from(102 { length: 32 * 1024 },103 (_, index) => index & 0xff,104 );105 const padded = padSyncFrame(payload);106 const chunks = chunkPaddedSyncFrame(padded);107108 expect(chunks).toHaveLength(2);109 expect(chunks[0]![0]).toBe(FRAME_CONTINUATION_MARKER);110 expect(chunks[0]!.byteLength).toBe(MAX_NOISE_PLAINTEXT_BYTES);111 expect(chunks[1]![0]).not.toBe(FRAME_CONTINUATION_MARKER);112113 const reassembled = new Uint8Array(114 chunks[0]!.byteLength - 1 + chunks[1]!.byteLength,115 );116 reassembled.set(chunks[0]!.subarray(1), 0);117 reassembled.set(chunks[1]!, chunks[0]!.byteLength - 1);118 expect(unpadSyncFrame(reassembled)).toEqual(payload);119 });120});