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.
- package/.claude/settings.local.json +12 -0
- package/README.md +65 -0
- package/dist/index.js +94 -0
- package/package.json +26 -0
- package/scripts/build.js +20 -0
- package/scripts/test.js +164 -0
- package/src/commands/config.ts +121 -0
- package/src/commands/down.ts +6 -0
- package/src/commands/groups.ts +68 -0
- package/src/commands/ls.ts +26 -0
- package/src/commands/up.ts +44 -0
- package/src/config/loader.ts +103 -0
- package/src/config/types.ts +20 -0
- package/src/index.ts +96 -0
- package/src/process/manager.ts +199 -0
- package/src/process/template.ts +72 -0
- package/tests/integration/blocking-processes-fixed.test.ts +255 -0
- package/tests/integration/blocking-processes.test.ts +497 -0
- package/tests/integration/commands.test.ts +674 -0
- package/tests/integration/config-loader.test.ts +426 -0
- package/tests/integration/process-manager.test.ts +391 -0
- package/tests/integration/template-expander.test.ts +362 -0
- package/tsconfig.json +15 -0
- package/usage.md +9 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for CLI commands
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the CLI command functionality including:
|
|
5
|
+
* - up command (starting process groups)
|
|
6
|
+
* - ls command (listing groups)
|
|
7
|
+
* - down command (stopping groups)
|
|
8
|
+
* - Error handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, before, after, mock } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { upCommand } from '../../src/commands/up.js';
|
|
17
|
+
import { lsCommand } from '../../src/commands/ls.js';
|
|
18
|
+
import { downCommand } from '../../src/commands/down.js';
|
|
19
|
+
import { groupsCommand } from '../../src/commands/groups.js';
|
|
20
|
+
|
|
21
|
+
describe('CLI Commands Integration Tests', () => {
|
|
22
|
+
let testConfigDir: string;
|
|
23
|
+
let testConfigPath: string;
|
|
24
|
+
let originalHomeDir: string;
|
|
25
|
+
let originalConsoleLog: typeof console.log;
|
|
26
|
+
let originalConsoleError: typeof console.error;
|
|
27
|
+
let logOutput: string[];
|
|
28
|
+
let errorOutput: string[];
|
|
29
|
+
|
|
30
|
+
before(() => {
|
|
31
|
+
// Create a temporary directory for test configs
|
|
32
|
+
testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cligr-cli-test-'));
|
|
33
|
+
testConfigPath = path.join(testConfigDir, '.cligr.yml');
|
|
34
|
+
|
|
35
|
+
// Mock os.homedir to return our test directory
|
|
36
|
+
originalHomeDir = os.homedir();
|
|
37
|
+
mock.method(os, 'homedir', () => testConfigDir);
|
|
38
|
+
|
|
39
|
+
// Capture console output
|
|
40
|
+
originalConsoleLog = console.log;
|
|
41
|
+
originalConsoleError = console.error;
|
|
42
|
+
logOutput = [];
|
|
43
|
+
errorOutput = [];
|
|
44
|
+
|
|
45
|
+
console.log = (...args: any[]) => {
|
|
46
|
+
logOutput.push(args.map(arg => String(arg)).join(' '));
|
|
47
|
+
};
|
|
48
|
+
console.error = (...args: any[]) => {
|
|
49
|
+
errorOutput.push(args.map(arg => String(arg)).join(' '));
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
after(() => {
|
|
54
|
+
// Restore console
|
|
55
|
+
console.log = originalConsoleLog;
|
|
56
|
+
console.error = originalConsoleError;
|
|
57
|
+
os.homedir = () => originalHomeDir;
|
|
58
|
+
|
|
59
|
+
// Clean up test directory
|
|
60
|
+
if (fs.existsSync(testConfigDir)) {
|
|
61
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function resetOutput() {
|
|
66
|
+
logOutput = [];
|
|
67
|
+
errorOutput = [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getLogOutput(): string {
|
|
71
|
+
return logOutput.join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getErrorOutput(): string {
|
|
75
|
+
return errorOutput.join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('upCommand', () => {
|
|
79
|
+
it('should start a group with echo commands (completes quickly)', { timeout: 5000 }, async () => {
|
|
80
|
+
const configContent = `
|
|
81
|
+
groups:
|
|
82
|
+
echo-test:
|
|
83
|
+
tool: echo
|
|
84
|
+
restart: no
|
|
85
|
+
items:
|
|
86
|
+
- hello
|
|
87
|
+
- world
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
91
|
+
|
|
92
|
+
// upCommand waits for Ctrl+C, so we need a different approach
|
|
93
|
+
// We'll test by verifying the config loads correctly
|
|
94
|
+
// The actual process spawning is tested in ProcessManager tests
|
|
95
|
+
|
|
96
|
+
// For now, just verify the command structure is valid
|
|
97
|
+
// Note: upCommand is designed to wait indefinitely, so we can't test it fully
|
|
98
|
+
// We just verify it doesn't throw on valid config
|
|
99
|
+
try {
|
|
100
|
+
// Create a timeout promise to avoid hanging
|
|
101
|
+
const timeoutPromise = new Promise<number>((resolve) => {
|
|
102
|
+
setTimeout(() => resolve(0), 100);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Start the command but it will wait for signals
|
|
106
|
+
// We'll just verify the initial setup doesn't throw
|
|
107
|
+
const { ConfigLoader } = await import('../../src/config/loader.js');
|
|
108
|
+
const loader = new ConfigLoader();
|
|
109
|
+
const { config } = loader.getGroup('echo-test');
|
|
110
|
+
|
|
111
|
+
assert.strictEqual(config.tool, 'echo');
|
|
112
|
+
assert.strictEqual(config.restart, 'no');
|
|
113
|
+
assert.strictEqual(config.items.length, 2);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
assert.fail(`Should not throw: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle missing config file', async () => {
|
|
120
|
+
// Remove config file
|
|
121
|
+
if (fs.existsSync(testConfigPath)) {
|
|
122
|
+
fs.unlinkSync(testConfigPath);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const exitCode = await upCommand('missing');
|
|
126
|
+
|
|
127
|
+
assert.strictEqual(exitCode, 1);
|
|
128
|
+
assert.ok(getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should handle unknown group', async () => {
|
|
132
|
+
const configContent = `
|
|
133
|
+
groups:
|
|
134
|
+
known-group:
|
|
135
|
+
tool: echo
|
|
136
|
+
restart: no
|
|
137
|
+
items:
|
|
138
|
+
- test
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
142
|
+
|
|
143
|
+
const exitCode = await upCommand('unknown-group');
|
|
144
|
+
|
|
145
|
+
assert.strictEqual(exitCode, 1);
|
|
146
|
+
assert.ok(getErrorOutput().includes('Unknown group'));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle restart policies correctly', { timeout: 3000 }, async () => {
|
|
150
|
+
const configContent = `
|
|
151
|
+
groups:
|
|
152
|
+
restart-test:
|
|
153
|
+
tool: echo
|
|
154
|
+
restart: yes
|
|
155
|
+
items:
|
|
156
|
+
- test
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
160
|
+
|
|
161
|
+
// Verify config loads with correct restart policy
|
|
162
|
+
const { ConfigLoader } = await import('../../src/config/loader.js');
|
|
163
|
+
const loader = new ConfigLoader();
|
|
164
|
+
const { config } = loader.getGroup('restart-test');
|
|
165
|
+
|
|
166
|
+
assert.strictEqual(config.restart, 'yes');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('lsCommand', () => {
|
|
171
|
+
it('should list items in a group', async () => {
|
|
172
|
+
const configContent = `
|
|
173
|
+
tools:
|
|
174
|
+
docker:
|
|
175
|
+
cmd: docker run -it --rm
|
|
176
|
+
|
|
177
|
+
groups:
|
|
178
|
+
web:
|
|
179
|
+
tool: docker
|
|
180
|
+
restart: unless-stopped
|
|
181
|
+
items:
|
|
182
|
+
- nginx,nginx,-p,80:80
|
|
183
|
+
- redis,redis
|
|
184
|
+
- postgres,postgres,-e,POSTGRES_PASSWORD=test
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
188
|
+
resetOutput();
|
|
189
|
+
|
|
190
|
+
const exitCode = await lsCommand('web');
|
|
191
|
+
|
|
192
|
+
assert.strictEqual(exitCode, 0);
|
|
193
|
+
const output = getLogOutput();
|
|
194
|
+
assert.ok(output.includes('Group: web'));
|
|
195
|
+
assert.ok(output.includes('Tool: docker'));
|
|
196
|
+
assert.ok(output.includes('Restart: unless-stopped'));
|
|
197
|
+
assert.ok(output.includes('Items:'));
|
|
198
|
+
assert.ok(output.includes('nginx'));
|
|
199
|
+
assert.ok(output.includes('redis'));
|
|
200
|
+
assert.ok(output.includes('postgres'));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should list items with correct arguments', async () => {
|
|
204
|
+
const configContent = `
|
|
205
|
+
groups:
|
|
206
|
+
test:
|
|
207
|
+
tool: echo
|
|
208
|
+
restart: no
|
|
209
|
+
items:
|
|
210
|
+
- item1,arg1,arg2
|
|
211
|
+
- item2,arg3,arg4,arg5
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
215
|
+
resetOutput();
|
|
216
|
+
|
|
217
|
+
const exitCode = await lsCommand('test');
|
|
218
|
+
|
|
219
|
+
assert.strictEqual(exitCode, 0);
|
|
220
|
+
const output = getLogOutput();
|
|
221
|
+
// Items are shown as full strings
|
|
222
|
+
assert.ok(output.includes('item1,arg1,arg2'));
|
|
223
|
+
assert.ok(output.includes('item2,arg3,arg4,arg5'));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle empty items list', async () => {
|
|
227
|
+
const configContent = `
|
|
228
|
+
groups:
|
|
229
|
+
empty:
|
|
230
|
+
tool: echo
|
|
231
|
+
restart: no
|
|
232
|
+
items: []
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
236
|
+
resetOutput();
|
|
237
|
+
|
|
238
|
+
const exitCode = await lsCommand('empty');
|
|
239
|
+
|
|
240
|
+
assert.strictEqual(exitCode, 0);
|
|
241
|
+
const output = getLogOutput();
|
|
242
|
+
assert.ok(output.includes('Group: empty'));
|
|
243
|
+
assert.ok(output.includes('Items:'));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should return error exit code for unknown group', async () => {
|
|
247
|
+
const configContent = `
|
|
248
|
+
groups:
|
|
249
|
+
known:
|
|
250
|
+
tool: echo
|
|
251
|
+
restart: no
|
|
252
|
+
items:
|
|
253
|
+
- test
|
|
254
|
+
`;
|
|
255
|
+
|
|
256
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
257
|
+
resetOutput();
|
|
258
|
+
|
|
259
|
+
const exitCode = await lsCommand('unknown');
|
|
260
|
+
|
|
261
|
+
assert.strictEqual(exitCode, 1);
|
|
262
|
+
assert.ok(getErrorOutput().includes('Unknown group'));
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should return error exit code for missing config', async () => {
|
|
266
|
+
if (fs.existsSync(testConfigPath)) {
|
|
267
|
+
fs.unlinkSync(testConfigPath);
|
|
268
|
+
}
|
|
269
|
+
resetOutput();
|
|
270
|
+
|
|
271
|
+
const exitCode = await lsCommand('any-group');
|
|
272
|
+
|
|
273
|
+
assert.strictEqual(exitCode, 1);
|
|
274
|
+
assert.ok(getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle groups without registered tools', async () => {
|
|
278
|
+
const configContent = `
|
|
279
|
+
groups:
|
|
280
|
+
direct:
|
|
281
|
+
tool: node
|
|
282
|
+
restart: no
|
|
283
|
+
items:
|
|
284
|
+
- server.js,3000
|
|
285
|
+
- worker.js
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
289
|
+
resetOutput();
|
|
290
|
+
|
|
291
|
+
const exitCode = await lsCommand('direct');
|
|
292
|
+
|
|
293
|
+
assert.strictEqual(exitCode, 0);
|
|
294
|
+
const output = getLogOutput();
|
|
295
|
+
assert.ok(output.includes('Tool: node'));
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('downCommand', () => {
|
|
300
|
+
it('should return success and display message', async () => {
|
|
301
|
+
resetOutput();
|
|
302
|
+
|
|
303
|
+
const exitCode = await downCommand('test-group');
|
|
304
|
+
|
|
305
|
+
assert.strictEqual(exitCode, 0);
|
|
306
|
+
const output = getLogOutput();
|
|
307
|
+
assert.ok(output.includes('down test-group'));
|
|
308
|
+
assert.ok(output.includes('will stop'));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should work with any group name', async () => {
|
|
312
|
+
const groupNames = ['test1', 'my-app', 'production', 'staging-env'];
|
|
313
|
+
|
|
314
|
+
for (const groupName of groupNames) {
|
|
315
|
+
resetOutput();
|
|
316
|
+
const exitCode = await downCommand(groupName);
|
|
317
|
+
assert.strictEqual(exitCode, 0);
|
|
318
|
+
assert.ok(getLogOutput().includes(groupName));
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('Command integration scenarios', () => {
|
|
324
|
+
it('should handle ls followed by up workflow', async () => {
|
|
325
|
+
const configContent = `
|
|
326
|
+
groups:
|
|
327
|
+
workflow-test:
|
|
328
|
+
tool: echo
|
|
329
|
+
restart: no
|
|
330
|
+
items:
|
|
331
|
+
- service1
|
|
332
|
+
- service2
|
|
333
|
+
- service3
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
337
|
+
|
|
338
|
+
// First list the group
|
|
339
|
+
resetOutput();
|
|
340
|
+
const lsExitCode = await lsCommand('workflow-test');
|
|
341
|
+
assert.strictEqual(lsExitCode, 0);
|
|
342
|
+
assert.ok(getLogOutput().includes('workflow-test'));
|
|
343
|
+
|
|
344
|
+
// Then verify config loads for up
|
|
345
|
+
const { ConfigLoader } = await import('../../src/config/loader.js');
|
|
346
|
+
const loader = new ConfigLoader();
|
|
347
|
+
const { config } = loader.getGroup('workflow-test');
|
|
348
|
+
|
|
349
|
+
assert.strictEqual(config.items.length, 3);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle multiple groups in config', async () => {
|
|
353
|
+
const configContent = `
|
|
354
|
+
groups:
|
|
355
|
+
group1:
|
|
356
|
+
tool: echo
|
|
357
|
+
restart: no
|
|
358
|
+
items:
|
|
359
|
+
- item1
|
|
360
|
+
|
|
361
|
+
group2:
|
|
362
|
+
tool: echo
|
|
363
|
+
restart: yes
|
|
364
|
+
items:
|
|
365
|
+
- item2
|
|
366
|
+
|
|
367
|
+
group3:
|
|
368
|
+
tool: echo
|
|
369
|
+
restart: unless-stopped
|
|
370
|
+
items:
|
|
371
|
+
- item3
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
375
|
+
|
|
376
|
+
// List each group
|
|
377
|
+
for (const groupName of ['group1', 'group2', 'group3']) {
|
|
378
|
+
resetOutput();
|
|
379
|
+
const exitCode = await lsCommand(groupName);
|
|
380
|
+
assert.strictEqual(exitCode, 0);
|
|
381
|
+
assert.ok(getLogOutput().includes(`Group: ${groupName}`));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('Error handling', () => {
|
|
387
|
+
it('should handle malformed YAML in config', async () => {
|
|
388
|
+
fs.writeFileSync(testConfigPath, 'invalid: yaml: [');
|
|
389
|
+
|
|
390
|
+
resetOutput();
|
|
391
|
+
|
|
392
|
+
const exitCode = await lsCommand('any-group');
|
|
393
|
+
|
|
394
|
+
assert.strictEqual(exitCode, 1);
|
|
395
|
+
assert.ok(getErrorOutput().includes('Invalid YAML') || getErrorOutput().includes('Config file not found') || getErrorOutput().includes('Unknown group'));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should handle config with missing required fields', async () => {
|
|
399
|
+
const configContent = `
|
|
400
|
+
groups:
|
|
401
|
+
incomplete:
|
|
402
|
+
tool: echo
|
|
403
|
+
# missing restart and items
|
|
404
|
+
`;
|
|
405
|
+
|
|
406
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
407
|
+
|
|
408
|
+
resetOutput();
|
|
409
|
+
|
|
410
|
+
const exitCode = await lsCommand('incomplete');
|
|
411
|
+
|
|
412
|
+
// Should either fail or handle gracefully
|
|
413
|
+
// The behavior depends on the validator implementation
|
|
414
|
+
assert.ok([0, 1].includes(exitCode));
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should handle config with extra unknown fields', async () => {
|
|
418
|
+
const configContent = `
|
|
419
|
+
extra_field: value
|
|
420
|
+
another_extra:
|
|
421
|
+
nested: value
|
|
422
|
+
|
|
423
|
+
groups:
|
|
424
|
+
with-extras:
|
|
425
|
+
tool: echo
|
|
426
|
+
restart: no
|
|
427
|
+
items:
|
|
428
|
+
- test
|
|
429
|
+
extra_item_field: should be ignored
|
|
430
|
+
`;
|
|
431
|
+
|
|
432
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
433
|
+
|
|
434
|
+
resetOutput();
|
|
435
|
+
|
|
436
|
+
const exitCode = await lsCommand('with-extras');
|
|
437
|
+
|
|
438
|
+
// Should succeed - extra fields are ignored
|
|
439
|
+
assert.strictEqual(exitCode, 0);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('Special characters and edge cases', () => {
|
|
444
|
+
it('should handle group names with hyphens and underscores', async () => {
|
|
445
|
+
const configContent = `
|
|
446
|
+
groups:
|
|
447
|
+
my-test-group_123:
|
|
448
|
+
tool: echo
|
|
449
|
+
restart: no
|
|
450
|
+
items:
|
|
451
|
+
- test
|
|
452
|
+
`;
|
|
453
|
+
|
|
454
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
455
|
+
|
|
456
|
+
resetOutput();
|
|
457
|
+
const exitCode = await lsCommand('my-test-group_123');
|
|
458
|
+
|
|
459
|
+
assert.strictEqual(exitCode, 0);
|
|
460
|
+
assert.ok(getLogOutput().includes('my-test-group_123'));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should handle item strings with special characters', async () => {
|
|
464
|
+
const configContent = `
|
|
465
|
+
groups:
|
|
466
|
+
special-chars:
|
|
467
|
+
tool: echo
|
|
468
|
+
restart: no
|
|
469
|
+
items:
|
|
470
|
+
- "service,with,commas"
|
|
471
|
+
- "another test"
|
|
472
|
+
- simple
|
|
473
|
+
`;
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
476
|
+
|
|
477
|
+
resetOutput();
|
|
478
|
+
const exitCode = await lsCommand('special-chars');
|
|
479
|
+
|
|
480
|
+
assert.strictEqual(exitCode, 0);
|
|
481
|
+
const output = getLogOutput();
|
|
482
|
+
assert.ok(output.includes('service,with,commas'));
|
|
483
|
+
assert.ok(output.includes('another test'));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle all restart policy values', async () => {
|
|
487
|
+
const configContent = `
|
|
488
|
+
groups:
|
|
489
|
+
restart-yes:
|
|
490
|
+
tool: echo
|
|
491
|
+
restart: yes
|
|
492
|
+
items:
|
|
493
|
+
- test
|
|
494
|
+
|
|
495
|
+
restart-no:
|
|
496
|
+
tool: echo
|
|
497
|
+
restart: no
|
|
498
|
+
items:
|
|
499
|
+
- test
|
|
500
|
+
|
|
501
|
+
restart-unless-stopped:
|
|
502
|
+
tool: echo
|
|
503
|
+
restart: unless-stopped
|
|
504
|
+
items:
|
|
505
|
+
- test
|
|
506
|
+
`;
|
|
507
|
+
|
|
508
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
509
|
+
|
|
510
|
+
for (const groupName of ['restart-yes', 'restart-no', 'restart-unless-stopped']) {
|
|
511
|
+
resetOutput();
|
|
512
|
+
const exitCode = await lsCommand(groupName);
|
|
513
|
+
assert.strictEqual(exitCode, 0);
|
|
514
|
+
assert.ok(getLogOutput().includes(groupName));
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('Tools configuration', () => {
|
|
520
|
+
it('should handle config with tools defined', async () => {
|
|
521
|
+
const configContent = `
|
|
522
|
+
tools:
|
|
523
|
+
docker:
|
|
524
|
+
cmd: docker run -it --rm
|
|
525
|
+
node:
|
|
526
|
+
cmd: node
|
|
527
|
+
python:
|
|
528
|
+
cmd: python3
|
|
529
|
+
|
|
530
|
+
groups:
|
|
531
|
+
docker-group:
|
|
532
|
+
tool: docker
|
|
533
|
+
restart: no
|
|
534
|
+
items:
|
|
535
|
+
- nginx,nginx
|
|
536
|
+
|
|
537
|
+
node-group:
|
|
538
|
+
tool: node
|
|
539
|
+
restart: yes
|
|
540
|
+
items:
|
|
541
|
+
- server.js
|
|
542
|
+
`;
|
|
543
|
+
|
|
544
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
545
|
+
|
|
546
|
+
// Test docker group
|
|
547
|
+
resetOutput();
|
|
548
|
+
let exitCode = await lsCommand('docker-group');
|
|
549
|
+
assert.strictEqual(exitCode, 0);
|
|
550
|
+
assert.ok(getLogOutput().includes('Tool: docker'));
|
|
551
|
+
|
|
552
|
+
// Test node group
|
|
553
|
+
resetOutput();
|
|
554
|
+
exitCode = await lsCommand('node-group');
|
|
555
|
+
assert.strictEqual(exitCode, 0);
|
|
556
|
+
assert.ok(getLogOutput().includes('Tool: node'));
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should handle group referencing undefined tool', async () => {
|
|
560
|
+
const configContent = `
|
|
561
|
+
groups:
|
|
562
|
+
missing-tool:
|
|
563
|
+
tool: nonexistent-tool
|
|
564
|
+
restart: no
|
|
565
|
+
items:
|
|
566
|
+
- test
|
|
567
|
+
`;
|
|
568
|
+
|
|
569
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
570
|
+
|
|
571
|
+
resetOutput();
|
|
572
|
+
const exitCode = await lsCommand('missing-tool');
|
|
573
|
+
|
|
574
|
+
// Should handle gracefully - tool is treated as direct executable
|
|
575
|
+
assert.strictEqual(exitCode, 0);
|
|
576
|
+
assert.ok(getLogOutput().includes('Tool: nonexistent-tool'));
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe('groupsCommand', () => {
|
|
581
|
+
it('should list group names in simple mode', async () => {
|
|
582
|
+
const configContent = `
|
|
583
|
+
groups:
|
|
584
|
+
web:
|
|
585
|
+
tool: docker
|
|
586
|
+
restart: no
|
|
587
|
+
items:
|
|
588
|
+
- nginx
|
|
589
|
+
- redis
|
|
590
|
+
|
|
591
|
+
database:
|
|
592
|
+
tool: docker
|
|
593
|
+
restart: yes
|
|
594
|
+
items:
|
|
595
|
+
- postgres
|
|
596
|
+
`;
|
|
597
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
598
|
+
resetOutput();
|
|
599
|
+
|
|
600
|
+
const exitCode = await groupsCommand(false);
|
|
601
|
+
|
|
602
|
+
assert.strictEqual(exitCode, 0);
|
|
603
|
+
const output = getLogOutput();
|
|
604
|
+
assert.ok(output.includes('web'));
|
|
605
|
+
assert.ok(output.includes('database'));
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should show detailed table in verbose mode', async () => {
|
|
609
|
+
const configContent = `
|
|
610
|
+
tools:
|
|
611
|
+
docker:
|
|
612
|
+
cmd: docker run
|
|
613
|
+
|
|
614
|
+
groups:
|
|
615
|
+
web:
|
|
616
|
+
tool: docker
|
|
617
|
+
restart: unless-stopped
|
|
618
|
+
items:
|
|
619
|
+
- nginx
|
|
620
|
+
- redis
|
|
621
|
+
- postgres
|
|
622
|
+
|
|
623
|
+
direct:
|
|
624
|
+
tool: node
|
|
625
|
+
restart: no
|
|
626
|
+
items:
|
|
627
|
+
- server.js
|
|
628
|
+
`;
|
|
629
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
630
|
+
resetOutput();
|
|
631
|
+
|
|
632
|
+
const exitCode = await groupsCommand(true);
|
|
633
|
+
|
|
634
|
+
assert.strictEqual(exitCode, 0);
|
|
635
|
+
const output = getLogOutput();
|
|
636
|
+
assert.ok(output.includes('GROUP'));
|
|
637
|
+
assert.ok(output.includes('TOOL'));
|
|
638
|
+
assert.ok(output.includes('RESTART'));
|
|
639
|
+
assert.ok(output.includes('ITEMS'));
|
|
640
|
+
assert.ok(output.includes('web'));
|
|
641
|
+
assert.ok(output.includes('docker'));
|
|
642
|
+
assert.ok(output.includes('unless-stopped'));
|
|
643
|
+
assert.ok(output.includes('3')); // item count for web
|
|
644
|
+
assert.ok(output.includes('direct'));
|
|
645
|
+
assert.ok(output.includes('node'));
|
|
646
|
+
assert.ok(output.includes('1')); // item count for direct
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should handle empty groups list', async () => {
|
|
650
|
+
const configContent = `
|
|
651
|
+
groups: {}
|
|
652
|
+
`;
|
|
653
|
+
fs.writeFileSync(testConfigPath, configContent);
|
|
654
|
+
resetOutput();
|
|
655
|
+
|
|
656
|
+
const exitCode = await groupsCommand(false);
|
|
657
|
+
|
|
658
|
+
assert.strictEqual(exitCode, 0);
|
|
659
|
+
assert.strictEqual(getLogOutput(), '');
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should handle missing config file', async () => {
|
|
663
|
+
if (fs.existsSync(testConfigPath)) {
|
|
664
|
+
fs.unlinkSync(testConfigPath);
|
|
665
|
+
}
|
|
666
|
+
resetOutput();
|
|
667
|
+
|
|
668
|
+
const exitCode = await groupsCommand(false);
|
|
669
|
+
|
|
670
|
+
assert.strictEqual(exitCode, 1);
|
|
671
|
+
assert.ok(getErrorOutput().includes('Config file not found'));
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
});
|