123456789101112131415161718192021import { describe, expect, it, vi } from "vitest";22import { generateKeyPair } from "@stablelib/x25519";23import { openChirpSyncSession } from "../../shared/chirp-sync-transport";242526272829interface MockWebSocketInstance {30 binaryType: string;31 onmessage: ((ev: MessageEvent) => void) | null;32 onerror: (() => void) | null;33 onclose: (() => void) | null;34 onopen: (() => void) | null;35 sentFrames: ArrayBuffer[];36 send(data: ArrayBuffer | Uint8Array): void;37 close(): void;38 simulateOpen(): void;39 simulateError(): void;40 simulateClose(): void;41 simulateMessage(data: ArrayBuffer): void;42}4344function createMockWebSocket(): MockWebSocketInstance {45 const instance: MockWebSocketInstance = {46 binaryType: "arraybuffer",47 onmessage: null,48 onerror: null,49 onclose: null,50 onopen: null,51 sentFrames: [],52 send(data: ArrayBuffer | Uint8Array) {53 if (data instanceof Uint8Array) {54 instance.sentFrames.push(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));55 } else {56 instance.sentFrames.push(data);57 }58 },59 close() {60 61 Promise.resolve().then(() => instance.simulateClose());62 },63 simulateOpen() {64 instance.onopen?.call(instance, new Event("open"));65 },66 simulateError() {67 instance.onerror?.call(instance);68 },69 simulateClose() {70 instance.onclose?.call(instance, new Event("close"));71 },72 simulateMessage(data: ArrayBuffer) {73 instance.onmessage?.call(instance, { data } as MessageEvent);74 },75 };76 return instance;77}787980818283describe("T-020: WebSocket sync-disconnect handling", () => {84 85 86 87 88 89 90 91 it("T-020a: WebSocket onerror during handshake → session promise rejects within event loop tick", async () => {92 let mockWs: MockWebSocketInstance;93 const MockWebSocketCtor = vi.fn().mockImplementation((_url: string) => {94 mockWs = createMockWebSocket();95 return mockWs;96 });97 vi.stubGlobal("WebSocket", MockWebSocketCtor);9899 const ourKp = generateKeyPair();100 const peerKp = generateKeyPair();101 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");102103 const sessionPromise = openChirpSyncSession(104 { ip: "127.0.0.1", port: 9999 },105 ourKp.secretKey,106 ourKp.publicKey,107 peerHex,108 );109110 111 await new Promise((r) => setTimeout(r, 0));112113 114 mockWs!.simulateOpen();115116 117 await new Promise((r) => setTimeout(r, 0));118119 120 121 mockWs!.simulateError();122123 124 await expect(sessionPromise).rejects.toThrow(/closed|error/i);125126 vi.unstubAllGlobals();127 });128129 130 131 132 133 it("T-020b: WebSocket onclose during handshake → session promise rejects", async () => {134 let mockWs: MockWebSocketInstance;135 const MockWebSocketCtor = vi.fn().mockImplementation((_url: string) => {136 mockWs = createMockWebSocket();137 return mockWs;138 });139 vi.stubGlobal("WebSocket", MockWebSocketCtor);140141 const ourKp = generateKeyPair();142 const peerKp = generateKeyPair();143 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");144145 const sessionPromise = openChirpSyncSession(146 { ip: "127.0.0.1", port: 9999 },147 ourKp.secretKey,148 ourKp.publicKey,149 peerHex,150 );151152 await new Promise((r) => setTimeout(r, 0));153 mockWs!.simulateOpen();154 await new Promise((r) => setTimeout(r, 0));155156 157 158 mockWs!.simulateClose();159160 await expect(sessionPromise).rejects.toThrow(/closed|error/i);161162 vi.unstubAllGlobals();163 });164165 166 167 168 169 170 171 it("T-020c: WebSocket connect timeout fires → openChirpSyncSession rejects with timeout error", async () => {172 let mockWs: MockWebSocketInstance;173 const MockWebSocketCtor = vi.fn().mockImplementation((_url: string) => {174 mockWs = createMockWebSocket();175 176 return mockWs;177 });178 vi.stubGlobal("WebSocket", MockWebSocketCtor);179180 const ourKp = generateKeyPair();181 const peerKp = generateKeyPair();182 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");183184 185 const sessionPromise = openChirpSyncSession(186 { ip: "127.0.0.1", port: 9999 },187 ourKp.secretKey,188 ourKp.publicKey,189 peerHex,190 { connectTimeoutMs: 10 },191 );192193 await expect(sessionPromise).rejects.toThrow(/timed out|timeout/i);194195 vi.unstubAllGlobals();196 }, 15_000);197198 199 200 201 202 203 204 it("T-020d: oversized frame received → session rejects (MV-3 frame-size guard)", async () => {205 let mockWs: MockWebSocketInstance;206 const MockWebSocketCtor = vi.fn().mockImplementation((_url: string) => {207 mockWs = createMockWebSocket();208 return mockWs;209 });210 vi.stubGlobal("WebSocket", MockWebSocketCtor);211212 const ourKp = generateKeyPair();213 const peerKp = generateKeyPair();214 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");215216 const sessionPromise = openChirpSyncSession(217 { ip: "127.0.0.1", port: 9999 },218 ourKp.secretKey,219 ourKp.publicKey,220 peerHex,221 );222223 await new Promise((r) => setTimeout(r, 0));224 mockWs!.simulateOpen();225 await new Promise((r) => setTimeout(r, 0));226227 228 const MAX_FRAME_BYTES = 10 * 1024 * 1024;229 const bigFrame = new ArrayBuffer(MAX_FRAME_BYTES + 1);230 mockWs!.simulateMessage(bigFrame);231232 await expect(sessionPromise).rejects.toThrow(/closed|error/i);233234 vi.unstubAllGlobals();235 });236});