Contents of smoke-test.mjs:
1: 
2: import WebSocket from 'ws';
3: import fetch from 'node-fetch';
4: import { exec, spawn } from 'child_process';
5: import { fileURLToPath } from 'url';
6: import path from 'path';
7: import { promisify } from 'util';
8: 
9: const __filename = fileURLToPath(import.meta.url);
10: const __dirname = path.dirname(__filename);
11: const execPromise = promisify(exec);
12: 
13: const startServer = () => {
14:     return new Promise((resolve, reject) => {
15:         const serverProcess = spawn('node', [path.join(__dirname, 'server.js')], { detached: true });
16: 
17:         serverProcess.stdout.on('data', (data) => {
18:             const output = data.toString();
19:             console.log(`Server: ${output}`);
20:             const match = output.match(/Server listening on port (\d+)/);
21:             if (match) {
22:                 const port = match[1];
23:                 console.log(`Server started on port ${port}`);
24:                 resolve({ serverProcess, port });
25:             }
26:         });
27: 
28:         serverProcess.stderr.on('data', (data) => {
29:             console.error(`Server Error: ${data.toString()}`);
30:         });
31: 
32:         serverProcess.on('error', (err) => {
33:             reject(err);
34:         });
35:     });
36: };
37: 
38: const startWorker = (workerScript, wsUrl) => {
39:     const workerProcess = spawn('node', [path.join(__dirname, workerScript)], { detached: true, env: { ...process.env, WS_URL: wsUrl } });
40:     workerProcess.stdout.on('data', (data) => console.log(`Worker (${workerScript}): ${data.toString()}`));
41:     workerProcess.stderr.on('data', (data) => console.error(`Worker Error (${workerScript}): ${data.toString()}`));
42:     return workerProcess;
43: };
44: 
45: const waitForWebSocketEvent = (ws, eventType, jobId, timeout = 10000) => {
46:     return new Promise((resolve, reject) => {
47:         let timer;
48:         const messageHandler = (message) => {
49:             const data = JSON.parse(message.data);
50:             if (data.event === eventType && (!jobId || data.job.id === jobId)) {
51:                 clearTimeout(timer);
52:                 ws.off('message', messageHandler);
53:                 resolve(data.job);
54:             }
55:         };
56: 
57:         ws.on('message', messageHandler);
58: 
59:         timer = setTimeout(() => {
60:             ws.off('message', messageHandler);
61:             reject(new Error(`Timeout waiting for ${eventType} for job ${jobId || 'any'}`));
62:         }, timeout);
63:     });
64: };
65: 
66: const runSmokeTest = async () => {
67:     let serverProcess, workerAProcess, workerBProcess, ws;
68:     try {
69:         console.log('Starting server...');
70:         const { serverProcess: sP, port } = await startServer();
71:         serverProcess = sP;
72:         const baseUrl = `http://localhost:${port}`;
73:         const wsUrl = `ws://localhost:${port}`;
74: 
75:         console.log('Starting workers...');
76:         workerAProcess = startWorker('workerA.js', wsUrl);
77:         workerBProcess = startWorker('workerB.js', wsUrl);
78: 
79:         // Give workers a moment to connect
80:         await new Promise(resolve => setTimeout(resolve, 3000));
81: 
82:         console.log('Connecting to WebSocket...');
83:         ws = new WebSocket(wsUrl);
84: 
85:         await new Promise((resolve, reject) => {
86:             ws.onopen = () => {
87:                 console.log('WebSocket connected.');
88:                 resolve();
89:             };
90:             ws.onerror = (err) => {
91:                 reject(new Error(`WebSocket connection error: ${err.message}`));
92:             };
93:         });
94: 
95:         console.log('Sending job_A...');
96:         const jobA_response = await fetch(`${baseUrl}/api/jobs`, {
97:             method: 'POST',
98:             headers: { 'Content-Type': 'application/json' },
99:             body: JSON.stringify({ type: 'workerA', payload: { task: 'process image' } })
100:         });
101:         const jobA = await jobA_response.json();
102:         console.log('Job A queued:', jobA);
103: 
104:         console.log('Sending job_B...');
105:         const jobB_response = await fetch(`${baseUrl}/api/jobs`, {
106:             method: 'POST',
107:             headers: { 'Content-Type': 'application/json' },
108:             body: JSON.stringify({ type: 'workerB', payload: { task: 'process data' } })
109:         });
110:         const jobB = await jobB_response.json();
111:         console.log('Job B queued:', jobB);
112: 
113:         const jobTransitions = {};
114: 
115:         ws.onmessage = (message) => {
116:             const data = JSON.parse(message.data);
117:             if (data.event.startsWith('job_') && data.job) {
118:                 const jobId = data.job.id;
119:                 if (!jobTransitions[jobId]) {
120:                     jobTransitions[jobId] = [];
121:                 }
122:                 jobTransitions[jobId].push(data.event);
123:                 console.log(`WS Event for ${jobId}: ${data.event}`);
124:             }
125:         };
126: 
127:         console.log('Waiting for job A to complete...');
128:         let finalJobA;
129:         while (!finalJobA || (finalJobA.status !== 'done' && finalJobA.status !== 'failed')) {
130:             finalJobA = await waitForWebSocketEvent(ws, 'job_done', jobA.id).catch(() => null) || await waitForWebSocketEvent(ws, 'job_failed', jobA.id).catch(() => null);
131:             if (finalJobA) {
132:                 console.log(`Job A final status via WS: ${finalJobA.status}`);
133:             }
134:             await new Promise(resolve => setTimeout(resolve, 500)); // Small delay to prevent tight loop
135:         }
136: 
137:         console.log('Waiting for job B to complete...');
138:         let finalJobB;
139:         while (!finalJobB || (finalJobB.status !== 'done' && finalJobB.status !== 'failed')) {
140:             finalJobB = await waitForWebSocketEvent(ws, 'job_done', jobB.id).catch(() => null) || await waitForWebSocketEvent(ws, 'job_failed', jobB.id).catch(() => null);
141:             if (finalJobB) {
142:                 console.log(`Job B final status via WS: ${finalJobB.status}`);
143:             }
144:             await new Promise(resolve => setTimeout(resolve, 500)); // Small delay to prevent tight loop
145:         }
146: 
147:         // Verify final status via HTTP GET
148:         console.log('Verifying job A final status via HTTP...');
149:         const jobA_final_http_response = await fetch(`${baseUrl}/api/jobs/${jobA.id}`);
150:         const jobA_final_http = await jobA_final_http_response.json();
151:         console.log('Job A final status via HTTP:', jobA_final_http.status);
152:         if (jobA_final_http.status === 'done' || jobA_final_http.status === 'failed') {
153:             console.log('Job A status verified.');
154:         } else {
155:             throw new Error(`Job A did not reach final state via HTTP. Current status: ${jobA_final_http.status}`);
156:         }
157: 
158:         console.log('Verifying job B final status via HTTP...');
159:         const jobB_final_http_response = await fetch(`${baseUrl}/api/jobs/${jobB.id}`);
160:         const jobB_final_http = await jobB_final_http_response.json();
161:         console.log('Job B final status via HTTP:', jobB_final_http.status);
162:         if (jobB_final_http.status === 'done' || jobB_final_http.status === 'failed') {
163:             console.log('Job B status verified.');
164:         } else {
165:             throw new Error(`Job B did not reach final state via HTTP. Current status: ${jobB_final_http.status}`);
166:         }
167: 
168:         console.log('Smoke test passed!');
169: 
170:     } catch (error) {
171:         console.error('Smoke test failed:', error);
172:         process.exit(1);
173:     } finally {
174:         console.log('Cleaning up...');
175:         if (ws) ws.close();
176:         if (serverProcess) process.kill(-serverProcess.pid); // Kill the entire process group
177:         if (workerAProcess) process.kill(-workerAProcess.pid);
178:         if (workerBProcess) process.kill(-workerBProcess.pid);
179:         console.log('Cleanup complete.');
180:     }
181: };
182: 
183: runSmokeTest();
184: 