cligr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Integration tests for blocking/long-running processes
3
+ *
4
+ * These tests verify the ProcessManager's ability to handle
5
+ * processes that run indefinitely or for extended periods.
6
+ */
7
+
8
+ import { describe, it, before, after } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import { spawn } from 'child_process';
11
+ import { ProcessManager } from '../../src/process/manager.js';
12
+ import type { ProcessItem } from '../../src/config/types.js';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+
17
+ describe('Blocking Processes Integration Tests', () => {
18
+ let manager: ProcessManager;
19
+ let testScriptsDir: string;
20
+
21
+ before(() => {
22
+ manager = new ProcessManager();
23
+
24
+ // Create a directory for test scripts
25
+ testScriptsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-blocking-test-'));
26
+ });
27
+
28
+ after(() => {
29
+ // Clean up any running processes
30
+ manager.killAll();
31
+
32
+ // Clean up test scripts directory
33
+ if (fs.existsSync(testScriptsDir)) {
34
+ fs.rmSync(testScriptsDir, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ function createInfiniteLoopScript(scriptName: string, delayMs: number = 1000): string {
39
+ const scriptPath = path.join(testScriptsDir, scriptName);
40
+ const scriptContent = `
41
+ // Infinite loop script - simulates a long-running process
42
+ let counter = 0;
43
+ const interval = setInterval(() => {
44
+ counter++;
45
+ console.log(\[${scriptName}] Running iteration: \${counter}\`);
46
+ }, ${delayMs});
47
+
48
+ // Keep process alive
49
+ process.on('SIGTERM', () => {
50
+ console.log(\[${scriptName}] Received SIGTERM, shutting down...\`);
51
+ clearInterval(interval);
52
+ process.exit(0);
53
+ });
54
+
55
+ process.on('SIGINT', () => {
56
+ console.log(\[${scriptName}] Received SIGINT, shutting down...\`);
57
+ clearInterval(interval);
58
+ process.exit(0);
59
+ });
60
+ `;
61
+ fs.writeFileSync(scriptPath, scriptContent);
62
+ return scriptPath;
63
+ }
64
+
65
+ function createBlockingScript(scriptName: string): string {
66
+ const scriptPath = path.join(testScriptsDir, scriptName);
67
+ const scriptContent = `
68
+ // Blocking script - simulates CPU-intensive work
69
+ console.log('Starting blocking process...');
70
+
71
+ // Simulate blocking work with periodic output
72
+ let iterations = 0;
73
+ const blockingWork = () => {
74
+ const start = Date.now();
75
+ while (Date.now() - start < 100) {
76
+ // Busy wait for 100ms - simulates blocking CPU work
77
+ Math.sqrt(Math.random() * 10000);
78
+ }
79
+ iterations++;
80
+ console.log(\[${scriptName}] Completed \${iterations} blocking cycles\`);
81
+
82
+ // Continue blocking work
83
+ if (iterations < 1000) {
84
+ setTimeout(blockingWork, 50);
85
+ }
86
+ };
87
+
88
+ blockingWork();
89
+
90
+ process.on('SIGTERM', () => {
91
+ console.log(\[${scriptName}] Shutting down after \${iterations} cycles\`);
92
+ process.exit(0);
93
+ });
94
+ `;
95
+ fs.writeFileSync(scriptPath, scriptContent);
96
+ return scriptPath;
97
+ }
98
+
99
+ function createServerScript(scriptName: string, port: number): string {
100
+ const scriptPath = path.join(testScriptsDir, scriptName);
101
+ const scriptContent = `
102
+ // HTTP server script - simulates a service that stays running
103
+ import http from 'http';
104
+
105
+ const server = http.createServer((req, res) => {
106
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
107
+ res.end('Hello from test server\\n');
108
+ });
109
+
110
+ server.listen(${port}, () => {
111
+ console.log(\`${scriptName} listening on port \${port}\`);
112
+ });
113
+
114
+ // Keep server running
115
+ process.on('SIGTERM', () => {
116
+ console.log(\`${scriptName} shutting down...\`);
117
+ server.close(() => {
118
+ process.exit(0);
119
+ });
120
+ });
121
+
122
+ process.on('SIGINT', () => {
123
+ console.log(\`${scriptName} shutting down...\`);
124
+ server.close(() => {
125
+ process.exit(0);
126
+ });
127
+ });
128
+ `;
129
+ fs.writeFileSync(scriptPath, scriptContent);
130
+ return scriptPath;
131
+ }
132
+
133
+ describe('Infinite loop processes', () => {
134
+ it('should manage infinite loop processes with setInterval', async () => {
135
+ const scriptPath = createInfiniteLoopScript('infinite-loop.js', 500);
136
+
137
+ const items: ProcessItem[] = [
138
+ { name: 'loop1', args: [scriptPath], fullCmd: `node ${scriptPath}` },
139
+ { name: 'loop2', args: [scriptPath], fullCmd: `node ${scriptPath}` }
140
+ ];
141
+
142
+ manager.spawnGroup('infinite-loop-group', items, 'no');
143
+
144
+ // Verify processes are running
145
+ assert.strictEqual(manager.isGroupRunning('infinite-loop-group'), true);
146
+
147
+ // Wait a bit to ensure processes started
148
+ await new Promise(resolve => setTimeout(resolve, 1500));
149
+
150
+ // Kill the group
151
+ manager.killGroup('infinite-loop-group');
152
+
153
+ // Verify processes were killed
154
+ assert.strictEqual(manager.isGroupRunning('infinite-loop-group'), false);
155
+ });
156
+
157
+ it('should handle infinite while(true) loop processes', async () => {
158
+ const scriptPath = path.join(testScriptsDir, 'while-true.js');
159
+ const scriptContent = `
160
+ // Infinite while loop
161
+ console.log('Starting infinite while(true) loop...');
162
+ let counter = 0;
163
+
164
+ while (true) {
165
+ counter++;
166
+ if (counter % 100000 === 0) {
167
+ console.log(\`While loop iteration: \${counter}\`);
168
+ // Small yield to prevent complete CPU lock
169
+ await new Promise(resolve => setImmediate(resolve));
170
+ }
171
+ if (counter > 1000000) break; // Safety break
172
+ }
173
+
174
+ process.on('SIGTERM', () => {
175
+ console.log('Process terminated after', counter, 'iterations');
176
+ process.exit(0);
177
+ });
178
+ `;
179
+ fs.writeFileSync(scriptPath, scriptContent);
180
+
181
+ const items: ProcessItem[] = [
182
+ { name: 'while-true', args: [scriptPath], fullCmd: `node ${scriptPath}` }
183
+ ];
184
+
185
+ manager.spawnGroup('while-true-group', items, 'no');
186
+
187
+ assert.strictEqual(manager.isGroupRunning('while-true-group'), true);
188
+
189
+ // Let it run briefly
190
+ await new Promise(resolve => setTimeout(resolve, 1000));
191
+
192
+ manager.killGroup('while-true-group');
193
+
194
+ assert.strictEqual(manager.isGroupRunning('while-true-group'), false);
195
+ });
196
+ });
197
+
198
+ describe('Blocking CPU-intensive processes', () => {
199
+ it('should manage blocking CPU processes', async () => {
200
+ const scriptPath = createBlockingScript('blocking-cpu.js');
201
+
202
+ const items: ProcessItem[] = [
203
+ { name: 'cpu-blocker', args: [scriptPath], fullCmd: `node ${scriptPath}` }
204
+ ];
205
+
206
+ manager.spawnGroup('blocking-group', items, 'no');
207
+
208
+ assert.strictEqual(manager.isGroupRunning('blocking-group'), true);
209
+
210
+ // Let the blocking work run
211
+ await new Promise(resolve => setTimeout(resolve, 2000));
212
+
213
+ // Verify it's still running despite blocking work
214
+ assert.strictEqual(manager.isGroupRunning('blocking-group'), true);
215
+
216
+ manager.killGroup('blocking-group');
217
+
218
+ assert.strictEqual(manager.isGroupRunning('blocking-group'), false);
219
+ });
220
+
221
+ it('should handle multiple blocking processes simultaneously', async () => {
222
+ const scriptPath = createBlockingScript('multi-blocker.js');
223
+
224
+ const items: ProcessItem[] = [
225
+ { name: 'blocker1', args: [scriptPath], fullCmd: `node ${scriptPath}` },
226
+ { name: 'blocker2', args: [scriptPath], fullCmd: `node ${scriptPath}` },
227
+ { name: 'blocker3', args: [scriptPath], fullCmd: `node ${scriptPath}` }
228
+ ];
229
+
230
+ manager.spawnGroup('multi-blocking-group', items, 'no');
231
+
232
+ assert.strictEqual(manager.isGroupRunning('multi-blocking-group'), true);
233
+
234
+ // Let them run
235
+ await new Promise(resolve => setTimeout(resolve, 1500));
236
+
237
+ manager.killGroup('multi-blocking-group');
238
+
239
+ assert.strictEqual(manager.isGroupRunning('multi-blocking-group'), false);
240
+ });
241
+ });
242
+
243
+ describe('Server processes', () => {
244
+ it('should manage HTTP server processes', async () => {
245
+ const port = 18080;
246
+ const scriptPath = createServerScript('test-server.js', port);
247
+
248
+ const items: ProcessItem[] = [
249
+ { name: 'http-server', args: [scriptPath], fullCmd: `node ${scriptPath}` }
250
+ ];
251
+
252
+ manager.spawnGroup('server-group', items, 'no');
253
+
254
+ assert.strictEqual(manager.isGroupRunning('server-group'), true);
255
+
256
+ // Wait for server to start
257
+ await new Promise(resolve => setTimeout(resolve, 1000));
258
+
259
+ // Optionally verify server is responding (would need fetch/http client)
260
+ // For now, just verify it's running
261
+ assert.strictEqual(manager.isGroupRunning('server-group'), true);
262
+
263
+ manager.killGroup('server-group');
264
+
265
+ assert.strictEqual(manager.isGroupRunning('server-group'), false);
266
+ });
267
+
268
+ it('should manage multiple server processes on different ports', async () => {
269
+ const ports = [18081, 18082, 18083];
270
+ const items: ProcessItem[] = [];
271
+
272
+ for (let i = 0; i < ports.length; i++) {
273
+ const scriptPath = createServerScript(`server-${i}.js`, ports[i]);
274
+ items.push({
275
+ name: `server-${ports[i]}`,
276
+ args: [scriptPath],
277
+ fullCmd: `node ${scriptPath}`
278
+ });
279
+ }
280
+
281
+ manager.spawnGroup('multi-server-group', items, 'no');
282
+
283
+ assert.strictEqual(manager.isGroupRunning('multi-server-group'), true);
284
+
285
+ // Wait for servers to start
286
+ await new Promise(resolve => setTimeout(resolve, 1500));
287
+
288
+ manager.killGroup('multi-server-group');
289
+
290
+ assert.strictEqual(manager.isGroupRunning('multi-server-group'), false);
291
+ });
292
+ });
293
+
294
+ describe('Long-running sleep processes', () => {
295
+ it('should manage long sleep processes', async () => {
296
+ // Use platform-specific sleep command
297
+ const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
298
+ const sleepArgs = process.platform === 'win32' ? ['/t', '60'] : ['60'];
299
+
300
+ const items: ProcessItem[] = [
301
+ {
302
+ name: 'long-sleep',
303
+ args: sleepArgs,
304
+ fullCmd: `${sleepCmd} ${sleepArgs.join(' ')}`
305
+ }
306
+ ];
307
+
308
+ manager.spawnGroup('sleep-group', items, 'no');
309
+
310
+ assert.strictEqual(manager.isGroupRunning('sleep-group'), true);
311
+
312
+ // Let it sleep briefly
313
+ await new Promise(resolve => setTimeout(resolve, 2000));
314
+
315
+ // Should still be running (sleeping for 60 seconds)
316
+ assert.strictEqual(manager.isGroupRunning('sleep-group'), true);
317
+
318
+ // Kill it before it completes
319
+ manager.killGroup('sleep-group');
320
+
321
+ assert.strictEqual(manager.isGroupRunning('sleep-group'), false);
322
+ });
323
+ });
324
+
325
+ describe('Process lifecycle with restart policies', () => {
326
+ it('should restart crashing processes with restart=yes', async () => {
327
+ const scriptPath = path.join(testScriptsDir, 'crash-restart.js');
328
+ const scriptContent = `
329
+ // Script that crashes after a short time
330
+ console.log('Starting crash test script...');
331
+ setTimeout(() => {
332
+ console.log('Crashing now!');
333
+ process.exit(1);
334
+ }, 500);
335
+ `;
336
+ fs.writeFileSync(scriptPath, scriptContent);
337
+
338
+ const items: ProcessItem[] = [
339
+ { name: 'crasher', args: [scriptPath], fullCmd: `node ${scriptPath}` }
340
+ ];
341
+
342
+ manager.spawnGroup('crash-restart-group', items, 'yes');
343
+
344
+ assert.strictEqual(manager.isGroupRunning('crash-restart-group'), true);
345
+
346
+ // Wait for first crash and restart attempt
347
+ await new Promise(resolve => setTimeout(resolve, 3000));
348
+
349
+ // Group should still be tracked (process may have restarted)
350
+ assert.strictEqual(manager.isGroupRunning('crash-restart-group'), true);
351
+
352
+ manager.killGroup('crash-restart-group');
353
+ });
354
+
355
+ it('should not restart with restart=no', async () => {
356
+ const scriptPath = path.join(testScriptsDir, 'no-restart.js');
357
+ const scriptContent = `
358
+ console.log('Starting no-restart test...');
359
+ setTimeout(() => {
360
+ console.log('Exiting gracefully');
361
+ process.exit(0);
362
+ }, 500);
363
+ `;
364
+ fs.writeFileSync(scriptPath, scriptContent);
365
+
366
+ const items: ProcessItem[] = [
367
+ { name: 'no-restart', args: [scriptPath], fullCmd: `node ${scriptPath}` }
368
+ ];
369
+
370
+ manager.spawnGroup('no-restart-group', items, 'no');
371
+
372
+ assert.strictEqual(manager.isGroupRunning('no-restart-group'), true);
373
+
374
+ // Wait for process to exit
375
+ await new Promise(resolve => setTimeout(resolve, 1500));
376
+
377
+ // Group tracking remains even after process exits
378
+ assert.strictEqual(manager.isGroupRunning('no-restart-group'), true);
379
+
380
+ manager.killGroup('no-restart-group');
381
+ });
382
+ });
383
+
384
+ describe('Stress tests with many blocking processes', () => {
385
+ it('should handle 10 simultaneous blocking processes', async () => {
386
+ const scriptPath = createInfiniteLoopScript('stress-test.js', 1000);
387
+ const items: ProcessItem[] = [];
388
+
389
+ for (let i = 0; i < 10; i++) {
390
+ items.push({
391
+ name: `stress-${i}`,
392
+ args: [scriptPath],
393
+ fullCmd: `node ${scriptPath}`
394
+ });
395
+ }
396
+
397
+ manager.spawnGroup('stress-group', items, 'no');
398
+
399
+ assert.strictEqual(manager.isGroupRunning('stress-group'), true);
400
+ assert.strictEqual(manager.getGroupStatus('stress-group').length, 10);
401
+
402
+ // Let them run
403
+ await new Promise(resolve => setTimeout(resolve, 2000));
404
+
405
+ manager.killGroup('stress-group');
406
+
407
+ assert.strictEqual(manager.isGroupRunning('stress-group'), false);
408
+ });
409
+ });
410
+
411
+ describe('Signal handling', () => {
412
+ it('should properly handle SIGTERM on blocking processes', async () => {
413
+ const scriptPath = path.join(testScriptsDir, 'sigterm-test.js');
414
+ const scriptContent = `
415
+ console.log('SIGTERM test started');
416
+
417
+ // Simulate blocking work
418
+ let running = true;
419
+ const workInterval = setInterval(() => {
420
+ if (running) {
421
+ console.log('Working...');
422
+ }
423
+ }, 500);
424
+
425
+ process.on('SIGTERM', () => {
426
+ console.log('Received SIGTERM, cleaning up...');
427
+ running = false;
428
+ clearInterval(workInterval);
429
+ setTimeout(() => {
430
+ console.log('Cleanup complete, exiting');
431
+ process.exit(0);
432
+ }, 100);
433
+ });
434
+
435
+ // Keep process alive indefinitely
436
+ console.log('Process waiting for signals...');
437
+ `;
438
+ fs.writeFileSync(scriptPath, scriptContent);
439
+
440
+ const items: ProcessItem[] = [
441
+ { name: 'sigterm-handler', args: [scriptPath], fullCmd: `node ${scriptPath}` }
442
+ ];
443
+
444
+ manager.spawnGroup('sigterm-group', items, 'no');
445
+
446
+ assert.strictEqual(manager.isGroupRunning('sigterm-group'), true);
447
+
448
+ await new Promise(resolve => setTimeout(resolve, 1500));
449
+
450
+ manager.killGroup('sigterm-group');
451
+
452
+ assert.strictEqual(manager.isGroupRunning('sigterm-group'), false);
453
+ });
454
+ });
455
+
456
+ describe('Mixed process types', () => {
457
+ it('should handle mix of servers, loops, and sleep processes', async () => {
458
+ const items: ProcessItem[] = [];
459
+
460
+ // Add a server
461
+ const serverScript = createServerScript('mixed-server.js', 18090);
462
+ items.push({
463
+ name: 'server',
464
+ args: [serverScript],
465
+ fullCmd: `node ${serverScript}`
466
+ });
467
+
468
+ // Add an infinite loop
469
+ const loopScript = createInfiniteLoopScript('mixed-loop.js', 800);
470
+ items.push({
471
+ name: 'loop',
472
+ args: [loopScript],
473
+ fullCmd: `node ${loopScript}`
474
+ });
475
+
476
+ // Add a sleep process
477
+ const sleepCmd = process.platform === 'win32' ? 'timeout' : 'sleep';
478
+ const sleepArgs = process.platform === 'win32' ? ['/t', '30'] : ['30'];
479
+ items.push({
480
+ name: 'sleep',
481
+ args: sleepArgs,
482
+ fullCmd: `${sleepCmd} ${sleepArgs.join(' ')}`
483
+ });
484
+
485
+ manager.spawnGroup('mixed-group', items, 'no');
486
+
487
+ assert.strictEqual(manager.isGroupRunning('mixed-group'), true);
488
+ assert.strictEqual(manager.getGroupStatus('mixed-group').length, 3);
489
+
490
+ await new Promise(resolve => setTimeout(resolve, 2000));
491
+
492
+ manager.killGroup('mixed-group');
493
+
494
+ assert.strictEqual(manager.isGroupRunning('mixed-group'), false);
495
+ });
496
+ });
497
+ });