123456789101112131415161718192021import { describe, expect, it, vi } from "vitest";22import { generateKeyPair } from "@stablelib/x25519";23import { openChirpSyncSession } from "./chirp-sync-transport";24import { SYNC_LIMITS } from "./sync-limits";252627282930interface MockWebSocketInstance {31 binaryType: string;32 onmessage: ((ev: MessageEvent) => void) | null;33 onerror: (() => void) | null;34 onclose: (() => void) | null;35 onopen: (() => void) | null;36 sentFrames: ArrayBuffer[];37 send(data: ArrayBuffer | Uint8Array): void;38 close(): void;39 simulateOpen(): void;40 simulateError(): void;41 simulateClose(): void;42 simulateMessage(data: ArrayBuffer): void;43}4445function createMockWebSocket(): MockWebSocketInstance {46 const instance: MockWebSocketInstance = {47 binaryType: "arraybuffer",48 onmessage: null,49 onerror: null,50 onclose: null,51 onopen: null,52 sentFrames: [],53 send(data: ArrayBuffer | Uint8Array) {54 if (data instanceof Uint8Array) {55 instance.sentFrames.push(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));56 } else {57 instance.sentFrames.push(data);58 }59 },60 close() {61 62 Promise.resolve().then(() => instance.simulateClose());63 },64 simulateOpen() {65 instance.onopen?.call(instance, new Event("open"));66 },67 simulateError() {68 instance.onerror?.call(instance);69 },70 simulateClose() {71 instance.onclose?.call(instance, new Event("close"));72 },73 simulateMessage(data: ArrayBuffer) {74 instance.onmessage?.call(instance, { data } as MessageEvent);75 },76 };77 return instance;78}798081828384describe("T-020: WebSocket sync-disconnect handling", () => {85 86 87 88 89 90 91 92 it("T-020a: WebSocket onerror during handshake → session promise rejects within event loop tick", async () => {93 let mockWs: MockWebSocketInstance;94 const MockWebSocketCtor = vi.fn().mockImplementation(function(_url: string) {95 mockWs = createMockWebSocket();96 return mockWs;97 });98 vi.stubGlobal("WebSocket", MockWebSocketCtor);99100 const ourKp = generateKeyPair();101 const peerKp = generateKeyPair();102 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");103104 const sessionPromise = openChirpSyncSession(105 { ip: "127.0.0.1", port: 9999 },106 ourKp.secretKey,107 ourKp.publicKey,108 peerHex,109 );110111 112 await new Promise((r) => setTimeout(r, 0));113114 115 mockWs!.simulateOpen();116117 118 await new Promise((r) => setTimeout(r, 0));119120 121 122 mockWs!.simulateError();123124 125 await expect(sessionPromise).rejects.toThrow(/closed|error/i);126127 vi.unstubAllGlobals();128 });129130 131 132 133 134 it("T-020b: WebSocket onclose during handshake → session promise rejects", async () => {135 let mockWs: MockWebSocketInstance;136 const MockWebSocketCtor = vi.fn().mockImplementation(function(_url: string) {137 mockWs = createMockWebSocket();138 return mockWs;139 });140 vi.stubGlobal("WebSocket", MockWebSocketCtor);141142 const ourKp = generateKeyPair();143 const peerKp = generateKeyPair();144 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");145146 const sessionPromise = openChirpSyncSession(147 { ip: "127.0.0.1", port: 9999 },148 ourKp.secretKey,149 ourKp.publicKey,150 peerHex,151 );152153 await new Promise((r) => setTimeout(r, 0));154 mockWs!.simulateOpen();155 await new Promise((r) => setTimeout(r, 0));156157 158 159 mockWs!.simulateClose();160161 await expect(sessionPromise).rejects.toThrow(/closed|error/i);162163 vi.unstubAllGlobals();164 });165166 167 168 169 170 171 172 it("T-020c: WebSocket connect timeout fires → openChirpSyncSession rejects with timeout error", async () => {173 let mockWs: MockWebSocketInstance;174 const MockWebSocketCtor = vi.fn().mockImplementation(function(_url: string) {175 mockWs = createMockWebSocket();176 177 return mockWs;178 });179 vi.stubGlobal("WebSocket", MockWebSocketCtor);180181 const ourKp = generateKeyPair();182 const peerKp = generateKeyPair();183 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");184185 186 const sessionPromise = openChirpSyncSession(187 { ip: "127.0.0.1", port: 9999 },188 ourKp.secretKey,189 ourKp.publicKey,190 peerHex,191 { connectTimeoutMs: 10 },192 );193194 await expect(sessionPromise).rejects.toThrow(/timed out|timeout/i);195196 vi.unstubAllGlobals();197 }, 15_000);198199 200 201 202 203 204 205 it("T-020d: oversized frame received → session rejects (MV-3 frame-size guard)", async () => {206 let mockWs: MockWebSocketInstance;207 const MockWebSocketCtor = vi.fn().mockImplementation(function(_url: string) {208 mockWs = createMockWebSocket();209 return mockWs;210 });211 vi.stubGlobal("WebSocket", MockWebSocketCtor);212213 const ourKp = generateKeyPair();214 const peerKp = generateKeyPair();215 const peerHex = Array.from(peerKp.publicKey).map(b => b.toString(16).padStart(2, "0")).join("");216217 const sessionPromise = openChirpSyncSession(218 { ip: "127.0.0.1", port: 9999 },219 ourKp.secretKey,220 ourKp.publicKey,221 peerHex,222 );223224 await new Promise((r) => setTimeout(r, 0));225 mockWs!.simulateOpen();226 await new Promise((r) => setTimeout(r, 0));227228 229 const bigFrame = new ArrayBuffer(SYNC_LIMITS.MAX_FRAME_BYTES + 1);230 mockWs!.simulateMessage(bigFrame);231232 await expect(sessionPromise).rejects.toThrow(/closed|error/i);233234 vi.unstubAllGlobals();235 });236});