#!/usr/bin/env node // scripts/test-websocket.ts // // Tests Socket.io WebSocket connection and event handling // Run: npx tsx scripts/test-websocket.ts // Requires server running. import 'dotenv/config'; import { io, type Socket } from 'socket.io-client'; const PORT = process.env.PORT || '3001'; const BASE_URL = `http://localhost:${PORT}`; const PASS = '\x1b[32m✅\x1b[0m'; const FAIL = '\x1b[31m❌\x1b[0m'; let passed = 0; let failed = 0; function pass(msg: string) { console.log(` ${PASS} ${msg}`); passed++; } function fail(msg: string) { console.log(` ${FAIL} ${msg}`); failed++; } function connectSocket(timeout = 5000): Promise { return new Promise((resolve, reject) => { const socket = io(BASE_URL, { path: '/ws/socket.io', transports: ['websocket', 'polling'], timeout, reconnection: false, }); const timer = setTimeout(() => { socket.disconnect(); reject(new Error('Connection timeout')); }, timeout); socket.on('connect', () => { clearTimeout(timer); resolve(socket); }); socket.on('connect_error', (err) => { clearTimeout(timer); reject(new Error(`Connection failed: ${err.message}`)); }); }); } async function main() { console.log('\n\x1b[1m🔌 ICC WebSocket (Socket.io) Tests\x1b[0m'); console.log(` Server: ${BASE_URL}`); console.log('═'.repeat(50)); // Check server is running first try { await fetch(`${BASE_URL}/api/health`, { signal: AbortSignal.timeout(2000) }); } catch { fail('Server not running'); console.log(' → Start with: cd packages/server && npx tsx src/index.ts'); process.exit(1); } // Test 1: Basic connection console.log('\n\x1b[1mConnection\x1b[0m'); let mainSocket: Socket | null = null; try { mainSocket = await connectSocket(5000); pass(`Socket.io connects successfully (id: ${mainSocket.id})`); } catch (error: any) { fail(`Socket.io connection failed: ${error.message}`); // Try polling-only as fallback info console.log(' Trying HTTP polling endpoint...'); try { const res = await fetch(`${BASE_URL}/ws/socket.io/?EIO=4&transport=polling`, { signal: AbortSignal.timeout(3000) }); if (res.ok) { pass('Socket.io HTTP polling endpoint responds'); } else { fail(`Socket.io polling returned ${res.status}`); } } catch (e: any) { fail(`Socket.io polling failed: ${e.message}`); } printSummary(); return; } // Test 2: Multiple concurrent connections (stress test) console.log('\n\x1b[1mStress Test\x1b[0m'); const clients: Socket[] = []; let connectFails = 0; for (let i = 0; i < 5; i++) { try { const client = await connectSocket(3000); clients.push(client); } catch { connectFails++; } } if (connectFails === 0) { pass('5 concurrent connections handled'); } else { fail(`${connectFails}/5 connections failed`); } // Clean up clients.forEach((c) => c.disconnect()); // Test 3: Graceful disconnect console.log('\n\x1b[1mDisconnect\x1b[0m'); mainSocket.disconnect(); await new Promise((r) => setTimeout(r, 500)); if (!mainSocket.connected) { pass('Clean disconnect'); } else { fail('Socket still connected after disconnect'); } // Test 4: Reconnect after disconnect try { const ws2 = await connectSocket(5000); pass('Reconnection after disconnect works'); ws2.disconnect(); } catch (e: any) { fail(`Reconnection failed: ${e.message}`); } printSummary(); } function printSummary() { console.log('\n' + '═'.repeat(50)); console.log(`\x1b[1m📋 Results: ${passed} passed, ${failed} failed\x1b[0m`); console.log('═'.repeat(50) + '\n'); process.exit(failed > 0 ? 1 : 0); } main().catch(console.error);