Monorepo for Aesthetic.Computer aesthetic.computer
at main 382 lines 12 kB view raw
1#!/usr/bin/env node 2/** 3 * 📦 WebSocket Module Loader Test 4 * 5 * Tests the WebSocket-based module streaming system: 6 * 1. Opens prompt in VS Code 7 * 2. Checks console logs for module loader activity 8 * 3. Verifies modules are being loaded via WebSocket 9 * 4. Tests cache hit/miss behavior 10 */ 11 12import { getArtery } from './artery-auto.mjs'; 13import WebSocket from 'ws'; 14 15// Colors 16const PURPLE_BG = '\x1b[45m'; 17const WHITE = '\x1b[97m'; 18const RESET = '\x1b[0m'; 19const GREEN = '\x1b[92m'; 20const CYAN = '\x1b[96m'; 21const YELLOW = '\x1b[93m'; 22const RED = '\x1b[91m'; 23const DIM = '\x1b[2m'; 24 25const testLog = (msg) => console.log(`${PURPLE_BG}${WHITE}📦${RESET} ${msg}`); 26const successLog = (msg) => console.log(`${GREEN}${msg}${RESET}`); 27const warnLog = (msg) => console.log(`${YELLOW}⚠️ ${msg}${RESET}`); 28const errorLog = (msg) => console.log(`${RED}${msg}${RESET}`); 29const dimLog = (msg) => console.log(`${DIM} ${msg}${RESET}`); 30 31// Test results 32const results = { 33 sessionServerConnected: false, 34 moduleLoaderInitialized: false, 35 modulesLoadedViaWS: [], 36 modulesLoadedViaHTTP: [], 37 cacheHits: 0, 38 errors: [], 39}; 40 41// Session server WebSocket URL 42const SESSION_WS_URL = 'wss://localhost:8889'; 43 44/** 45 * Test 1: Direct session server module streaming 46 */ 47async function testSessionServerModules() { 48 testLog('Test 1: Session Server Module Streaming\n'); 49 50 return new Promise((resolve) => { 51 const ws = new WebSocket(SESSION_WS_URL, { rejectUnauthorized: false }); 52 let modulesReceived = 0; 53 const testModules = ['lib/parse.mjs', 'lib/num.mjs', 'lib/geo.mjs']; 54 55 const timeout = setTimeout(() => { 56 warnLog('Session server test timed out'); 57 ws.close(); 58 resolve(false); 59 }, 10000); 60 61 ws.on('open', () => { 62 results.sessionServerConnected = true; 63 dimLog('Connected to session server'); 64 65 // Request module list first 66 ws.send(JSON.stringify({ type: 'module:list' })); 67 }); 68 69 ws.on('message', (data) => { 70 try { 71 const msg = JSON.parse(data.toString()); 72 73 if (msg.type === 'module:list') { 74 dimLog(`Available modules: ${msg.modules.length}`); 75 msg.modules.forEach(m => { 76 dimLog(` ${m.path} - ${(m.size / 1024).toFixed(1)}KB`); 77 }); 78 79 // Now request specific modules 80 testModules.forEach(path => { 81 ws.send(JSON.stringify({ type: 'module:request', path })); 82 }); 83 } 84 85 if (msg.type === 'module:response') { 86 modulesReceived++; 87 dimLog(`Received: ${msg.path} (${msg.content.length} bytes, hash: ${msg.hash})`); 88 results.modulesLoadedViaWS.push(msg.path); 89 90 if (modulesReceived >= testModules.length) { 91 clearTimeout(timeout); 92 ws.close(); 93 successLog(`Session server streaming works! (${modulesReceived} modules)\n`); 94 resolve(true); 95 } 96 } 97 98 if (msg.type === 'module:error') { 99 errorLog(`Module error: ${msg.path} - ${msg.error}`); 100 results.errors.push(msg.error); 101 } 102 } catch (e) { 103 // Ignore non-JSON messages 104 } 105 }); 106 107 ws.on('error', (err) => { 108 clearTimeout(timeout); 109 errorLog(`Session server connection failed: ${err.message}`); 110 resolve(false); 111 }); 112 }); 113} 114 115/** 116 * Test 2: Browser module loader via CDP 117 */ 118async function testBrowserModuleLoader() { 119 testLog('Test 2: Browser Module Loader Integration\n'); 120 121 try { 122 const Artery = await getArtery(); 123 124 // Open panel if needed 125 await Artery.openPanelStandalone?.(); 126 await new Promise(r => setTimeout(r, 500)); 127 128 // Connect via CDP 129 const client = new Artery(); 130 await client.connect(); 131 dimLog('Connected via CDP'); 132 133 // Enable console logging 134 await client.send('Runtime.enable'); 135 await client.send('Console.enable'); 136 137 // Collect console logs 138 const logs = []; 139 client.on('Console.messageAdded', (params) => { 140 const text = params.message.text; 141 const level = params.message.level || 'log'; 142 logs.push({ level, text }); 143 if (text.includes('📦')) { 144 dimLog(`[console] ${text}`); 145 } 146 }); 147 148 client.on('Runtime.consoleAPICalled', (params) => { 149 const text = params.args.map(arg => { 150 if (arg.value !== undefined) return String(arg.value); 151 if (arg.description) return arg.description; 152 return JSON.stringify(arg); 153 }).join(' '); 154 const level = params.type || 'log'; 155 logs.push({ level, text }); 156 if (text.includes('📦')) { 157 dimLog(`[console] ${text}`); 158 } 159 }); 160 161 // Navigate to prompt (triggers full boot) 162 await client.jump('prompt'); 163 dimLog('Navigated to prompt'); 164 165 // Wait for boot to complete 166 await new Promise(r => setTimeout(r, 4000)); 167 168 // Check if module loader initialized 169 const loaderStatus = await client.send('Runtime.evaluate', { 170 expression: ` 171 (function() { 172 const loader = window.acModuleLoader; 173 if (!loader) return { initialized: false, reason: 'not found' }; 174 return { 175 initialized: true, 176 connected: loader.connected, 177 cachedModules: Array.from(loader.modules.keys()), 178 wsUrl: loader.wsUrl 179 }; 180 })() 181 `, 182 returnByValue: true 183 }); 184 185 const status = loaderStatus.result?.value; 186 if (status?.initialized) { 187 results.moduleLoaderInitialized = true; 188 dimLog(`Module loader initialized`); 189 dimLog(` WebSocket URL: ${status.wsUrl}`); 190 dimLog(` Connected: ${status.connected}`); 191 dimLog(` Cached modules: ${status.cachedModules.length}`); 192 status.cachedModules.forEach(m => dimLog(` - ${m}`)); 193 194 if (status.connected) { 195 successLog('Module loader is connected via WebSocket!\n'); 196 } else { 197 warnLog('Module loader fell back to HTTP (WebSocket not connected)\n'); 198 } 199 } else { 200 warnLog(`Module loader not initialized: ${status?.reason || 'unknown'}\n`); 201 } 202 203 // Check boot timings 204 const timings = await client.send('Runtime.evaluate', { 205 expression: `window._bootTimings || []`, 206 returnByValue: true 207 }); 208 209 const bootTimings = timings.result?.value || []; 210 const wsLoads = bootTimings.filter(t => t.message.includes('via ws')); 211 const httpLoads = bootTimings.filter(t => t.message.includes('loading') && !t.message.includes('via ws')); 212 213 if (wsLoads.length > 0) { 214 dimLog(`Modules loaded via WebSocket: ${wsLoads.length}`); 215 wsLoads.forEach(t => dimLog(` ${t.message} (+${t.elapsed}ms)`)); 216 results.modulesLoadedViaWS.push(...wsLoads.map(t => t.message)); 217 } 218 219 // Check for any module loader errors in logs 220 const errorLogs = logs.filter(l => l.level === 'error' && l.text.includes('module')); 221 if (errorLogs.length > 0) { 222 errorLogs.forEach(e => results.errors.push(e.text)); 223 } 224 225 // Test cache behavior by requesting a module that should be cached 226 const cacheTest = await client.send('Runtime.evaluate', { 227 expression: ` 228 (async function() { 229 const loader = window.acModuleLoader; 230 if (!loader) return { error: 'no loader' }; 231 232 // Check if parse.mjs is cached 233 const cached = loader.modules.get('lib/parse.mjs'); 234 if (cached) { 235 return { cached: true, hash: cached.hash }; 236 } 237 238 // Try to load it 239 const start = performance.now(); 240 const url = await loader.load('lib/parse.mjs'); 241 const elapsed = performance.now() - start; 242 243 return { 244 cached: false, 245 loaded: true, 246 elapsed: Math.round(elapsed), 247 url: url.substring(0, 50) + '...' 248 }; 249 })() 250 `, 251 returnByValue: true, 252 awaitPromise: true 253 }); 254 255 const cacheResult = cacheTest.result?.value; 256 if (cacheResult?.cached) { 257 results.cacheHits++; 258 dimLog(`Cache hit for lib/parse.mjs (hash: ${cacheResult.hash})`); 259 } else if (cacheResult?.loaded) { 260 dimLog(`Loaded lib/parse.mjs in ${cacheResult.elapsed}ms`); 261 } 262 263 // Also test IndexedDB persistence while we have the panel open 264 const dbCheck = await client.send('Runtime.evaluate', { 265 expression: ` 266 (async function() { 267 return new Promise((resolve) => { 268 const req = indexedDB.open('ac-module-cache', 2); 269 req.onerror = () => resolve({ error: 'IndexedDB not available' }); 270 req.onsuccess = () => { 271 const db = req.result; 272 if (!db.objectStoreNames.contains('modules')) { 273 resolve({ error: 'No modules store' }); 274 return; 275 } 276 const tx = db.transaction('modules', 'readonly'); 277 const store = tx.objectStore('modules'); 278 const getAllReq = store.getAll(); 279 280 getAllReq.onsuccess = () => { 281 const modules = getAllReq.result || []; 282 resolve({ 283 count: modules.length, 284 modules: modules.map(m => ({ 285 path: m.path, 286 hash: m.hash, 287 size: m.content?.length || 0, 288 cachedAt: m.cachedAt 289 })) 290 }); 291 }; 292 }; 293 }); 294 })() 295 `, 296 returnByValue: true, 297 awaitPromise: true 298 }); 299 300 const cacheStatus = dbCheck.result?.value; 301 if (cacheStatus?.count > 0) { 302 successLog(`IndexedDB cache has ${cacheStatus.count} modules persisted`); 303 cacheStatus.modules.forEach(m => { 304 dimLog(` ${m.path} - ${(m.size / 1024).toFixed(1)}KB`); 305 }); 306 } else if (cacheStatus?.error) { 307 warnLog(`IndexedDB: ${cacheStatus.error}`); 308 } else { 309 dimLog('IndexedDB cache empty (WebSocket-only mode)'); 310 } 311 312 // Clean up 313 client.close(); 314 await Artery.closePanelStandalone?.(); 315 316 return true; 317 318 } catch (err) { 319 errorLog(`Browser test failed: ${err.message}`); 320 results.errors.push(err.message); 321 return false; 322 } 323} 324 325/** 326 * Print test summary 327 */ 328function printSummary() { 329 console.log('\n' + '═'.repeat(50)); 330 console.log(`${PURPLE_BG}${WHITE} 📦 WebSocket Module Loader Test Summary ${RESET}`); 331 console.log('═'.repeat(50) + '\n'); 332 333 const checks = [ 334 ['Session server connection', results.sessionServerConnected], 335 ['Module loader initialized', results.moduleLoaderInitialized], 336 ['Modules loaded via WebSocket', results.modulesLoadedViaWS.length > 0], 337 ['No errors', results.errors.length === 0], 338 ]; 339 340 checks.forEach(([name, passed]) => { 341 const icon = passed ? `${GREEN}${RESET}` : `${RED}${RESET}`; 342 console.log(` ${icon} ${name}`); 343 }); 344 345 if (results.modulesLoadedViaWS.length > 0) { 346 console.log(`\n ${CYAN}WebSocket loaded:${RESET}`); 347 [...new Set(results.modulesLoadedViaWS)].slice(0, 5).forEach(m => { 348 dimLog(` ${m}`); 349 }); 350 } 351 352 if (results.errors.length > 0) { 353 console.log(`\n ${RED}Errors:${RESET}`); 354 results.errors.forEach(e => dimLog(` ${e}`)); 355 } 356 357 console.log(); 358 359 const allPassed = checks.every(([, passed]) => passed); 360 return allPassed; 361} 362 363async function main() { 364 console.log('\n' + '═'.repeat(50)); 365 console.log(`${PURPLE_BG}${WHITE} 📦 WebSocket Module Loader Test ${RESET}`); 366 console.log('═'.repeat(50) + '\n'); 367 368 // Run tests 369 await testSessionServerModules(); 370 await testBrowserModuleLoader(); 371 372 // Print summary 373 const passed = printSummary(); 374 375 process.exit(passed ? 0 : 1); 376} 377 378main().catch(err => { 379 errorLog(`Test crashed: ${err.message}`); 380 console.error(err); 381 process.exit(1); 382});