Monorepo for Aesthetic.Computer
aesthetic.computer
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});