1import { afterEach, describe, expect, it, vi } from "vitest";2import {3 MAX_PAIRING_FRAME_BYTES,4 MAX_PAIRING_INBOX_BYTES,5 MAX_PAIRING_INBOX_MESSAGES,6 PAIRING_RESOURCE_LIMIT,7 openRelayPairingSocket,8 type PairingSocket,9} from "./pairing-dial";1011121314interface FakeSocket {15 binaryType: string;16 onmessage: ((ev: { data: unknown }) => void) | null;17 onerror: (() => void) | null;18 onclose: ((ev: { code: number }) => void) | null;19 onopen: (() => void) | null;20 sent: unknown[];21 closeCalls: Array<{ code?: number; reason?: string }>;22 send(data: unknown): void;23 close(code?: number, reason?: string): void;24 simulateOpen(): void;25 simulateMessage(data: unknown): void;26 simulateClose(code?: number): void;27}2829let fake: FakeSocket;3031function createFake(): FakeSocket {32 const sock: FakeSocket = {33 binaryType: "",34 onmessage: null,35 onerror: null,36 onclose: null,37 onopen: null,38 sent: [],39 closeCalls: [],40 send(data: unknown) {41 sock.sent.push(data);42 },43 close(code?: number, reason?: string) {44 sock.closeCalls.push({ code, reason });45 46 queueMicrotask(() => sock.simulateClose(code));47 },48 simulateOpen() {49 sock.onopen?.();50 },51 simulateMessage(data: unknown) {52 sock.onmessage?.({ data });53 },54 simulateClose(code = 1000) {55 sock.onclose?.({ code });56 },57 };58 return sock;59}6061async function openSocket(): Promise<PairingSocket> {62 const pending = openRelayPairingSocket("wss://relay.example.test/room");63 fake.simulateOpen();64 return pending;65}6667function frame(n: number): ArrayBuffer {68 return new Uint8Array(n).buffer as ArrayBuffer;69}7071afterEach(() => {72 vi.unstubAllGlobals();73});7475describe("PB-26-205 pairing relay inbox limits", () => {76 it("accepts a frame of exactly MAX_PAIRING_FRAME_BYTES", async () => {77 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {78 fake = createFake();79 return fake;80 }));81 const sock = await openSocket();82 fake.simulateMessage(frame(MAX_PAIRING_FRAME_BYTES));83 const got = await sock.receive();84 expect(got.byteLength).toBe(MAX_PAIRING_FRAME_BYTES);85 expect(fake.closeCalls).toEqual([]);86 });8788 it("rejects a max+1 frame with a 1009 close before copying", async () => {89 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {90 fake = createFake();91 return fake;92 }));93 const sock = await openSocket();94 const pending = sock.receive();95 const rejection = expect(pending).rejects.toThrow(PAIRING_RESOURCE_LIMIT);96 fake.simulateMessage(frame(MAX_PAIRING_FRAME_BYTES + 1));97 await rejection;98 expect(fake.closeCalls[0]?.code).toBe(1009);99 100 await expect(sock.receive()).rejects.toThrow(PAIRING_RESOURCE_LIMIT);101 });102103 it("rejects non-ArrayBuffer frames without copying", async () => {104 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {105 fake = createFake();106 return fake;107 }));108 const sock = await openSocket();109 const pending = sock.receive();110 const rejection = expect(pending).rejects.toThrow(PAIRING_RESOURCE_LIMIT);111 fake.simulateMessage("not-binary");112 await rejection;113 expect(fake.closeCalls[0]?.code).toBe(1009);114 });115116 it("closes when individually valid frames exceed the inbox byte budget", async () => {117 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {118 fake = createFake();119 return fake;120 }));121 const sock = await openSocket();122 const frames = Math.floor(MAX_PAIRING_INBOX_BYTES / MAX_PAIRING_FRAME_BYTES);123 for (let i = 0; i < frames; i++) fake.simulateMessage(frame(MAX_PAIRING_FRAME_BYTES));124 125 fake.simulateMessage(frame(1));126 expect(fake.closeCalls[0]?.code).toBe(1009);127 await expect(sock.receive()).rejects.toThrow(PAIRING_RESOURCE_LIMIT);128 });129130 it("enforces the message-count limit with tiny frames", async () => {131 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {132 fake = createFake();133 return fake;134 }));135 const sock = await openSocket();136 for (let i = 0; i < MAX_PAIRING_INBOX_MESSAGES; i++) fake.simulateMessage(frame(1));137 expect(fake.closeCalls).toEqual([]);138 fake.simulateMessage(frame(1));139 expect(fake.closeCalls[0]?.code).toBe(1009);140 await expect(sock.receive()).rejects.toThrow(PAIRING_RESOURCE_LIMIT);141 });142143 it("dequeue relieves the budget so later valid frames are accepted", async () => {144 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {145 fake = createFake();146 return fake;147 }));148 const sock = await openSocket();149 for (let i = 0; i < MAX_PAIRING_INBOX_MESSAGES; i++) fake.simulateMessage(frame(1));150 const first = await sock.receive();151 expect(first.byteLength).toBe(1);152 153 fake.simulateMessage(frame(1));154 expect(fake.closeCalls).toEqual([]);155 const rest: number[] = [];156 for (let i = 0; i < MAX_PAIRING_INBOX_MESSAGES; i++) rest.push((await sock.receive()).byteLength);157 expect(rest).toHaveLength(MAX_PAIRING_INBOX_MESSAGES);158 });159160 it("gives every pending reader the same terminal error and ignores later events", async () => {161 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {162 fake = createFake();163 return fake;164 }));165 const sock = await openSocket();166 const r1 = sock.receive();167 const r2 = sock.receive();168 fake.simulateMessage(frame(MAX_PAIRING_FRAME_BYTES + 1));169 await expect(r1).rejects.toThrow(PAIRING_RESOURCE_LIMIT);170 await expect(r2).rejects.toThrow(PAIRING_RESOURCE_LIMIT);171 172 fake.simulateMessage(frame(1));173 fake.simulateClose(1009);174 await new Promise((r) => setTimeout(r, 0));175 expect(fake.closeCalls).toHaveLength(1);176 await expect(sock.receive()).rejects.toThrow(PAIRING_RESOURCE_LIMIT);177 expect(sock.closedBeforeFrame()).toBe(false);178 });179180 it("refuses oversized outbound frames before sending", async () => {181 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {182 fake = createFake();183 return fake;184 }));185 const sock = await openSocket();186 expect(() => sock.send(new Uint8Array(frame(MAX_PAIRING_FRAME_BYTES + 1)))).toThrow(187 PAIRING_RESOURCE_LIMIT,188 );189 expect(fake.sent).toHaveLength(0);190 191 sock.send(new Uint8Array(frame(16)));192 expect(fake.sent).toHaveLength(1);193 });194195 it("keeps normal pairing and wrong-code behavior unchanged", async () => {196 vi.stubGlobal("WebSocket", vi.fn().mockImplementation(function () {197 fake = createFake();198 return fake;199 }));200 const sock = await openSocket();201 fake.simulateMessage(frame(64));202 fake.simulateMessage(frame(128));203 expect((await sock.receive()).byteLength).toBe(64);204 expect((await sock.receive()).byteLength).toBe(128);205 206 const pending = sock.receive();207 const rejection = expect(pending).rejects.toThrow("socket closed");208 fake.simulateClose(1000);209 await rejection;210 expect(sock.closedBeforeFrame()).toBe(true);211 });212});