@eldrforge/git-tools 0.1.3 → 0.1.4

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.
@@ -1,1035 +0,0 @@
1
- import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { run, runWithDryRunSupport, runWithInheritedStdio, runSecure, runSecureWithInheritedStdio, validateGitRef, validateFilePath } from '../src/child';
3
- import { exec, spawn } from 'child_process';
4
- import { promisify } from 'util';
5
- import { getLogger } from '../src/logger';
6
-
7
- // Mock the dependencies
8
- vi.mock('child_process');
9
- vi.mock('util');
10
- vi.mock('../src/logger');
11
-
12
- describe('child.ts - run function', () => {
13
- const mockExec = vi.mocked(exec);
14
- const mockPromisify = vi.mocked(promisify);
15
- const mockExecPromise = vi.fn();
16
- const mockLogger = {
17
- info: vi.fn(),
18
- error: vi.fn(),
19
- warn: vi.fn(),
20
- debug: vi.fn(),
21
- verbose: vi.fn(),
22
- silly: vi.fn()
23
- };
24
- const mockGetLogger = vi.mocked(getLogger);
25
-
26
- beforeEach(() => {
27
- vi.clearAllMocks();
28
- mockPromisify.mockReturnValue(mockExecPromise);
29
- mockGetLogger.mockReturnValue(mockLogger as any);
30
- });
31
-
32
- afterEach(() => {
33
- vi.restoreAllMocks();
34
- });
35
-
36
- test('should execute command successfully and return stdout and stderr', async () => {
37
- const expectedResult = {
38
- stdout: 'Command executed successfully',
39
- stderr: ''
40
- };
41
-
42
- mockExecPromise.mockResolvedValue(expectedResult);
43
-
44
- const result = await run('echo "hello world"');
45
-
46
- expect(mockPromisify).toHaveBeenCalledWith(mockExec);
47
- expect(mockExecPromise).toHaveBeenCalledWith('echo "hello world"', expect.objectContaining({
48
- encoding: 'utf8'
49
- }));
50
- expect(result).toEqual(expectedResult);
51
- });
52
-
53
- test('should log verbose messages during successful execution', async () => {
54
- const command = 'echo "test"';
55
- const options = { cwd: '/test/dir', env: { TEST: 'value' } };
56
- const expectedResult = { stdout: 'test output', stderr: 'warning message' };
57
-
58
- mockExecPromise.mockResolvedValue(expectedResult);
59
-
60
- await run(command, options);
61
-
62
- expect(mockLogger.verbose).toHaveBeenCalledWith(`Executing command: ${command}`);
63
- expect(mockLogger.verbose).toHaveBeenCalledWith('Working directory: /test/dir');
64
- expect(mockLogger.verbose).toHaveBeenCalledWith('Environment variables: 1 variables');
65
- expect(mockLogger.verbose).toHaveBeenCalledWith('Command completed successfully');
66
- expect(mockLogger.verbose).toHaveBeenCalledWith('stdout: test output');
67
- expect(mockLogger.verbose).toHaveBeenCalledWith('stderr: warning message');
68
- });
69
-
70
- test('should log default working directory when cwd is not specified', async () => {
71
- const expectedResult = { stdout: 'test', stderr: '' };
72
- mockExecPromise.mockResolvedValue(expectedResult);
73
-
74
- await run('test-command');
75
-
76
- expect(mockLogger.verbose).toHaveBeenCalledWith(`Working directory: ${process.cwd()}`);
77
- });
78
-
79
- test('should count environment variables correctly', async () => {
80
- const options = {
81
- env: {
82
- NODE_ENV: 'test',
83
- DEBUG: 'true',
84
- PATH: '/usr/bin'
85
- }
86
- };
87
- const expectedResult = { stdout: 'test', stderr: '' };
88
- mockExecPromise.mockResolvedValue(expectedResult);
89
-
90
- await run('env-test', options);
91
-
92
- expect(mockLogger.verbose).toHaveBeenCalledWith('Environment variables: 3 variables');
93
- });
94
-
95
- test('should use process.env count when env option not provided', async () => {
96
- const expectedResult = { stdout: 'test', stderr: '' };
97
- mockExecPromise.mockResolvedValue(expectedResult);
98
-
99
- await run('env-test');
100
-
101
- expect(mockLogger.verbose).toHaveBeenCalledWith(`Environment variables: ${Object.keys(process.env).length} variables`);
102
- });
103
-
104
- test('should not log stderr verbose message when stderr is empty', async () => {
105
- const expectedResult = { stdout: 'output', stderr: '' };
106
- mockExecPromise.mockResolvedValue(expectedResult);
107
-
108
- await run('test-command');
109
-
110
- expect(mockLogger.verbose).toHaveBeenCalledWith('stdout: output');
111
- // Should not call verbose for empty stderr
112
- expect(mockLogger.verbose).not.toHaveBeenCalledWith('stderr: ');
113
- });
114
-
115
- test('should execute command with custom options', async () => {
116
- const expectedResult = {
117
- stdout: 'Command output',
118
- stderr: ''
119
- };
120
- const options = {
121
- cwd: '/custom/directory',
122
- env: { NODE_ENV: 'test' },
123
- timeout: 5000
124
- };
125
-
126
- mockExecPromise.mockResolvedValue(expectedResult);
127
-
128
- const result = await run('npm --version', options);
129
-
130
- expect(mockExecPromise).toHaveBeenCalledWith('npm --version', expect.objectContaining({
131
- ...options,
132
- encoding: 'utf8'
133
- }));
134
- expect(result).toEqual(expectedResult);
135
- });
136
-
137
- test('should handle commands that produce stderr output', async () => {
138
- const expectedResult = {
139
- stdout: '',
140
- stderr: 'Warning: deprecated feature used'
141
- };
142
-
143
- mockExecPromise.mockResolvedValue(expectedResult);
144
-
145
- const result = await run('some-command-with-warnings');
146
-
147
- expect(result).toEqual(expectedResult);
148
- expect(result.stderr).toBe('Warning: deprecated feature used');
149
- });
150
-
151
- test('should handle commands that produce both stdout and stderr', async () => {
152
- const expectedResult = {
153
- stdout: 'Success message',
154
- stderr: 'Warning message'
155
- };
156
-
157
- mockExecPromise.mockResolvedValue(expectedResult);
158
-
159
- const result = await run('command-with-mixed-output');
160
-
161
- expect(result).toEqual(expectedResult);
162
- expect(result.stdout).toBe('Success message');
163
- expect(result.stderr).toBe('Warning message');
164
- });
165
-
166
- test('should reject when command execution fails', async () => {
167
- const error = new Error('Command failed');
168
- mockExecPromise.mockRejectedValue(error);
169
-
170
- await expect(run('invalid-command')).rejects.toThrow('Command failed');
171
- expect(mockExecPromise).toHaveBeenCalledWith('invalid-command', expect.objectContaining({
172
- encoding: 'utf8'
173
- }));
174
- });
175
-
176
- test('should log detailed error information when command fails', async () => {
177
- const command = 'failing-command';
178
- const error = Object.assign(new Error('Command execution failed'), {
179
- code: 1,
180
- signal: null,
181
- stdout: 'partial output',
182
- stderr: 'error details'
183
- });
184
-
185
- mockExecPromise.mockRejectedValue(error);
186
-
187
- await expect(run(command)).rejects.toThrow('Command execution failed');
188
-
189
- expect(mockLogger.error).toHaveBeenCalledWith(`Command failed: ${command}`);
190
- expect(mockLogger.error).toHaveBeenCalledWith('Error: Command execution failed');
191
- expect(mockLogger.error).toHaveBeenCalledWith('Exit code: 1');
192
- expect(mockLogger.error).toHaveBeenCalledWith('Signal: null');
193
- expect(mockLogger.error).toHaveBeenCalledWith('stdout: partial output');
194
- expect(mockLogger.error).toHaveBeenCalledWith('stderr: error details');
195
- });
196
-
197
- test('should not log stdout/stderr in error when they are empty', async () => {
198
- const command = 'failing-command';
199
- const error = Object.assign(new Error('Command execution failed'), {
200
- code: 1,
201
- signal: null
202
- // No stdout/stderr properties
203
- });
204
-
205
- mockExecPromise.mockRejectedValue(error);
206
-
207
- await expect(run(command)).rejects.toThrow('Command execution failed');
208
-
209
- expect(mockLogger.error).toHaveBeenCalledWith(`Command failed: ${command}`);
210
- expect(mockLogger.error).toHaveBeenCalledWith('Error: Command execution failed');
211
- expect(mockLogger.error).toHaveBeenCalledWith('Exit code: 1');
212
- expect(mockLogger.error).toHaveBeenCalledWith('Signal: null');
213
- // Should not log stdout/stderr when they don't exist
214
- expect(mockLogger.error).not.toHaveBeenCalledWith('stdout: undefined');
215
- expect(mockLogger.error).not.toHaveBeenCalledWith('stderr: undefined');
216
- });
217
-
218
- test('should log only existing stdout/stderr in error scenarios', async () => {
219
- const command = 'partial-failure';
220
- const error = Object.assign(new Error('Partial failure'), {
221
- code: 1,
222
- signal: null,
223
- stdout: 'some output',
224
- // stderr property missing
225
- });
226
-
227
- mockExecPromise.mockRejectedValue(error);
228
-
229
- await expect(run(command)).rejects.toThrow('Partial failure');
230
-
231
- expect(mockLogger.error).toHaveBeenCalledWith('stdout: some output');
232
- expect(mockLogger.error).not.toHaveBeenCalledWith('stderr: undefined');
233
- });
234
-
235
- test('should handle error with undefined code and signal', async () => {
236
- const command = 'undefined-error';
237
- const error = Object.assign(new Error('Undefined error'), {
238
- // code and signal are undefined
239
- });
240
-
241
- mockExecPromise.mockRejectedValue(error);
242
-
243
- await expect(run(command)).rejects.toThrow('Undefined error');
244
-
245
- expect(mockLogger.error).toHaveBeenCalledWith('Exit code: undefined');
246
- expect(mockLogger.error).toHaveBeenCalledWith('Signal: undefined');
247
- });
248
-
249
- test('should handle command with exit code error', async () => {
250
- const error = Object.assign(new Error('Command failed with exit code 1'), {
251
- code: 1,
252
- killed: false,
253
- signal: null,
254
- cmd: 'failing-command'
255
- });
256
-
257
- mockExecPromise.mockRejectedValue(error);
258
-
259
- await expect(run('failing-command')).rejects.toMatchObject({
260
- message: 'Command failed with exit code 1',
261
- code: 1,
262
- killed: false,
263
- signal: null,
264
- cmd: 'failing-command'
265
- });
266
- });
267
-
268
- test('should handle timeout errors', async () => {
269
- const timeoutError = Object.assign(new Error('Command timed out'), {
270
- killed: true,
271
- signal: 'SIGTERM',
272
- code: null
273
- });
274
-
275
- mockExecPromise.mockRejectedValue(timeoutError);
276
-
277
- await expect(run('long-running-command', { timeout: 1000 })).rejects.toMatchObject({
278
- message: 'Command timed out',
279
- killed: true,
280
- signal: 'SIGTERM'
281
- });
282
- });
283
-
284
- test('should handle empty command string', async () => {
285
- const expectedResult = {
286
- stdout: '',
287
- stderr: ''
288
- };
289
-
290
- mockExecPromise.mockResolvedValue(expectedResult);
291
-
292
- const result = await run('');
293
-
294
- expect(mockExecPromise).toHaveBeenCalledWith('', expect.objectContaining({
295
- encoding: 'utf8'
296
- }));
297
- expect(result).toEqual(expectedResult);
298
- });
299
-
300
- test('should handle commands with special characters', async () => {
301
- const command = 'echo "Hello & goodbye; echo $HOME | grep user"';
302
- const expectedResult = {
303
- stdout: 'Hello & goodbye; echo $HOME | grep user',
304
- stderr: ''
305
- };
306
-
307
- mockExecPromise.mockResolvedValue(expectedResult);
308
-
309
- const result = await run(command);
310
-
311
- expect(mockExecPromise).toHaveBeenCalledWith(command, expect.objectContaining({
312
- encoding: 'utf8'
313
- }));
314
- expect(result).toEqual(expectedResult);
315
- });
316
-
317
- test('should handle large output', async () => {
318
- const largeOutput = 'x'.repeat(10000);
319
- const expectedResult = {
320
- stdout: largeOutput,
321
- stderr: ''
322
- };
323
-
324
- mockExecPromise.mockResolvedValue(expectedResult);
325
-
326
- const result = await run('command-with-large-output');
327
-
328
- expect(result.stdout).toBe(largeOutput);
329
- expect(result.stdout.length).toBe(10000);
330
- });
331
-
332
- test('should handle unicode characters in output', async () => {
333
- const unicodeOutput = '🚀 Deployment successful! 中文测试 émojis 🎉';
334
- const expectedResult = {
335
- stdout: unicodeOutput,
336
- stderr: ''
337
- };
338
-
339
- mockExecPromise.mockResolvedValue(expectedResult);
340
-
341
- const result = await run('echo "unicode test"');
342
-
343
- expect(result.stdout).toBe(unicodeOutput);
344
- });
345
-
346
- test('should handle multiple consecutive calls', async () => {
347
- const results = [
348
- { stdout: 'First command', stderr: '' },
349
- { stdout: 'Second command', stderr: '' },
350
- { stdout: 'Third command', stderr: '' }
351
- ];
352
-
353
- mockExecPromise
354
- .mockResolvedValueOnce(results[0])
355
- .mockResolvedValueOnce(results[1])
356
- .mockResolvedValueOnce(results[2]);
357
-
358
- const [result1, result2, result3] = await Promise.all([
359
- run('command1'),
360
- run('command2'),
361
- run('command3')
362
- ]);
363
-
364
- expect(result1).toEqual(results[0]);
365
- expect(result2).toEqual(results[1]);
366
- expect(result3).toEqual(results[2]);
367
- expect(mockExecPromise).toHaveBeenCalledTimes(3);
368
- });
369
-
370
- test('should preserve options object immutability', async () => {
371
- const options = {
372
- cwd: '/test',
373
- env: { TEST: 'value' }
374
- };
375
- const originalOptions = { ...options };
376
-
377
- mockExecPromise.mockResolvedValue({ stdout: 'test', stderr: '' });
378
-
379
- await run('test-command', options);
380
-
381
- expect(options).toEqual(originalOptions);
382
- expect(mockExecPromise).toHaveBeenCalledWith('test-command', expect.objectContaining({
383
- ...options,
384
- encoding: 'utf8'
385
- }));
386
- });
387
-
388
- test('should handle maxBuffer option', async () => {
389
- const options = {
390
- maxBuffer: 1024 * 1024 // 1MB
391
- };
392
-
393
- mockExecPromise.mockResolvedValue({ stdout: 'test', stderr: '' });
394
-
395
- await run('command-with-large-buffer', options);
396
-
397
- expect(mockExecPromise).toHaveBeenCalledWith('command-with-large-buffer', expect.objectContaining({
398
- ...options,
399
- encoding: 'utf8'
400
- }));
401
- });
402
-
403
- test('should handle shell option', async () => {
404
- const options = {
405
- shell: '/bin/bash'
406
- };
407
-
408
- mockExecPromise.mockResolvedValue({ stdout: 'test', stderr: '' });
409
-
410
- await run('command-with-shell', options);
411
-
412
- expect(mockExecPromise).toHaveBeenCalledWith('command-with-shell', expect.objectContaining({
413
- ...options,
414
- encoding: 'utf8'
415
- }));
416
- });
417
-
418
- test('should handle process signals', async () => {
419
- const signalError = Object.assign(new Error('Process terminated'), {
420
- killed: true,
421
- signal: 'SIGINT',
422
- code: null
423
- });
424
-
425
- mockExecPromise.mockRejectedValue(signalError);
426
-
427
- await expect(run('interruptible-command')).rejects.toMatchObject({
428
- message: 'Process terminated',
429
- killed: true,
430
- signal: 'SIGINT'
431
- });
432
- });
433
-
434
- test('should handle commands with environment variables', async () => {
435
- const options = {
436
- env: {
437
- ...process.env,
438
- NODE_ENV: 'test',
439
- DEBUG: 'true'
440
- }
441
- };
442
-
443
- mockExecPromise.mockResolvedValue({ stdout: 'env test', stderr: '' });
444
-
445
- await run('env-command', options);
446
-
447
- expect(mockExecPromise).toHaveBeenCalledWith('env-command', expect.objectContaining({
448
- ...options,
449
- encoding: 'utf8'
450
- }));
451
- });
452
-
453
- test('should handle cwd option', async () => {
454
- const options = {
455
- cwd: '/custom/working/directory'
456
- };
457
-
458
- mockExecPromise.mockResolvedValue({ stdout: 'pwd output', stderr: '' });
459
-
460
- await run('pwd', options);
461
-
462
- expect(mockExecPromise).toHaveBeenCalledWith('pwd', expect.objectContaining({
463
- ...options,
464
- encoding: 'utf8'
465
- }));
466
- });
467
-
468
- test('should handle windowsHide option', async () => {
469
- const options = {
470
- windowsHide: true
471
- };
472
-
473
- mockExecPromise.mockResolvedValue({ stdout: 'hidden window', stderr: '' });
474
-
475
- await run('windows-command', options);
476
-
477
- expect(mockExecPromise).toHaveBeenCalledWith('windows-command', expect.objectContaining({
478
- ...options,
479
- encoding: 'utf8'
480
- }));
481
- });
482
- });
483
-
484
- describe('child.ts - runWithDryRunSupport function', () => {
485
- const mockExec = vi.mocked(exec);
486
- const mockPromisify = vi.mocked(promisify);
487
- const mockExecPromise = vi.fn();
488
- const mockLogger = {
489
- info: vi.fn(),
490
- error: vi.fn(),
491
- warn: vi.fn(),
492
- debug: vi.fn(),
493
- verbose: vi.fn(),
494
- silly: vi.fn()
495
- };
496
- const mockGetLogger = vi.mocked(getLogger);
497
-
498
- beforeEach(() => {
499
- vi.clearAllMocks();
500
- mockPromisify.mockReturnValue(mockExecPromise);
501
- mockGetLogger.mockReturnValue(mockLogger as any);
502
- });
503
-
504
- afterEach(() => {
505
- vi.restoreAllMocks();
506
- });
507
-
508
- test('should log command and return empty result when isDryRun is true', async () => {
509
- const command = 'echo "test command"';
510
- const isDryRun = true;
511
-
512
- const result = await runWithDryRunSupport(command, isDryRun);
513
-
514
- expect(mockGetLogger).toHaveBeenCalled();
515
- expect(mockLogger.info).toHaveBeenCalledWith(`DRY RUN: Would execute command: ${command}`);
516
- expect(result).toEqual({ stdout: '', stderr: '' });
517
- expect(mockExecPromise).not.toHaveBeenCalled();
518
- });
519
-
520
- test('should log command and return empty result when isDryRun is true with options', async () => {
521
- const command = 'npm install';
522
- const isDryRun = true;
523
- const options = { cwd: '/test/directory' };
524
-
525
- const result = await runWithDryRunSupport(command, isDryRun, options);
526
-
527
- expect(mockLogger.info).toHaveBeenCalledWith(`DRY RUN: Would execute command: ${command}`);
528
- expect(result).toEqual({ stdout: '', stderr: '' });
529
- expect(mockExecPromise).not.toHaveBeenCalled();
530
- });
531
-
532
- test('should execute command normally when isDryRun is false', async () => {
533
- const command = 'echo "real command"';
534
- const isDryRun = false;
535
- const expectedResult = { stdout: 'real command output', stderr: '' };
536
-
537
- mockExecPromise.mockResolvedValue(expectedResult);
538
-
539
- const result = await runWithDryRunSupport(command, isDryRun);
540
-
541
- expect(mockLogger.info).not.toHaveBeenCalled();
542
- expect(mockExecPromise).toHaveBeenCalledWith(command, expect.objectContaining({
543
- encoding: 'utf8'
544
- }));
545
- expect(result).toEqual(expectedResult);
546
- });
547
-
548
- test('should execute command with options when isDryRun is false', async () => {
549
- const command = 'npm test';
550
- const isDryRun = false;
551
- const options = {
552
- cwd: '/project/root',
553
- env: { NODE_ENV: 'test' },
554
- timeout: 30000
555
- };
556
- const expectedResult = { stdout: 'test output', stderr: 'test warnings' };
557
-
558
- mockExecPromise.mockResolvedValue(expectedResult);
559
-
560
- const result = await runWithDryRunSupport(command, isDryRun, options);
561
-
562
- expect(mockExecPromise).toHaveBeenCalledWith(command, expect.objectContaining({
563
- ...options,
564
- encoding: 'utf8'
565
- }));
566
- expect(result).toEqual(expectedResult);
567
- });
568
-
569
- test('should propagate errors when isDryRun is false and command fails', async () => {
570
- const command = 'failing-command';
571
- const isDryRun = false;
572
- const error = new Error('Command execution failed');
573
-
574
- mockExecPromise.mockRejectedValue(error);
575
-
576
- await expect(runWithDryRunSupport(command, isDryRun)).rejects.toThrow('Command execution failed');
577
- expect(mockExecPromise).toHaveBeenCalledWith(command, expect.objectContaining({
578
- encoding: 'utf8'
579
- }));
580
- });
581
-
582
- test('should handle complex commands in dry run mode', async () => {
583
- const command = 'git commit -m "Complex commit with special chars: $VAR & symbols"';
584
- const isDryRun = true;
585
-
586
- const result = await runWithDryRunSupport(command, isDryRun);
587
-
588
- expect(mockLogger.info).toHaveBeenCalledWith(`DRY RUN: Would execute command: ${command}`);
589
- expect(result).toEqual({ stdout: '', stderr: '' });
590
- });
591
-
592
- test('should handle empty command in dry run mode', async () => {
593
- const command = '';
594
- const isDryRun = true;
595
-
596
- const result = await runWithDryRunSupport(command, isDryRun);
597
-
598
- expect(mockLogger.info).toHaveBeenCalledWith('DRY RUN: Would execute command: ');
599
- expect(result).toEqual({ stdout: '', stderr: '' });
600
- });
601
-
602
- test('should handle commands with unicode in dry run mode', async () => {
603
- const command = 'echo "🚀 Deploy to production! 中文测试"';
604
- const isDryRun = true;
605
-
606
- const result = await runWithDryRunSupport(command, isDryRun);
607
-
608
- expect(mockLogger.info).toHaveBeenCalledWith(`DRY RUN: Would execute command: ${command}`);
609
- expect(result).toEqual({ stdout: '', stderr: '' });
610
- });
611
-
612
- test('should call getLogger only once per invocation in dry run mode', async () => {
613
- const command = 'test command';
614
- const isDryRun = true;
615
-
616
- await runWithDryRunSupport(command, isDryRun);
617
-
618
- expect(mockGetLogger).toHaveBeenCalledTimes(1);
619
- expect(mockLogger.info).toHaveBeenCalledTimes(1);
620
- });
621
-
622
- test('should call getLogger when isDryRun is false', async () => {
623
- const command = 'test command';
624
- const isDryRun = false;
625
-
626
- mockExecPromise.mockResolvedValue({ stdout: 'output', stderr: '' });
627
-
628
- await runWithDryRunSupport(command, isDryRun);
629
-
630
- expect(mockGetLogger).toHaveBeenCalledTimes(2); // Called once in runWithDryRunSupport and once in run
631
- expect(mockLogger.info).not.toHaveBeenCalled(); // Should not call info for dry run messages
632
- });
633
-
634
- test('should use inherited stdio when useInheritedStdio is true and isDryRun is false', async () => {
635
- const command = 'echo "test with stdio"';
636
- const isDryRun = false;
637
- const useInheritedStdio = true;
638
-
639
- // Mock runWithInheritedStdio
640
- const mockSpawn = vi.mocked(spawn);
641
- const mockChild = {
642
- on: vi.fn((event, callback) => {
643
- if (event === 'close') {
644
- setTimeout(() => callback(0), 10);
645
- }
646
- return mockChild;
647
- })
648
- };
649
- mockSpawn.mockReturnValue(mockChild as any);
650
-
651
- const result = await runWithDryRunSupport(command, isDryRun, {}, useInheritedStdio);
652
-
653
- expect(mockSpawn).toHaveBeenCalledWith('echo', ['"test', 'with', 'stdio"'], {
654
- shell: false,
655
- stdio: 'inherit'
656
- });
657
- expect(result).toEqual({ stdout: '', stderr: '' });
658
- expect(mockExecPromise).not.toHaveBeenCalled(); // Should not call normal run
659
- });
660
-
661
- test('should use inherited stdio with options when useInheritedStdio is true', async () => {
662
- const command = 'npm test';
663
- const isDryRun = false;
664
- const options = {
665
- cwd: '/test/directory',
666
- env: { NODE_ENV: 'test' }
667
- };
668
- const useInheritedStdio = true;
669
-
670
- const mockSpawn = vi.mocked(spawn);
671
- const mockChild = {
672
- on: vi.fn((event, callback) => {
673
- if (event === 'close') {
674
- setTimeout(() => callback(0), 10);
675
- }
676
- return mockChild;
677
- })
678
- };
679
- mockSpawn.mockReturnValue(mockChild as any);
680
-
681
- const result = await runWithDryRunSupport(command, isDryRun, options, useInheritedStdio);
682
-
683
- expect(mockSpawn).toHaveBeenCalledWith('npm', ['test'], {
684
- ...options,
685
- shell: false,
686
- stdio: 'inherit'
687
- });
688
- expect(result).toEqual({ stdout: '', stderr: '' });
689
- });
690
-
691
- test('should handle inherited stdio failure when useInheritedStdio is true', async () => {
692
- const command = 'failing-command';
693
- const isDryRun = false;
694
- const useInheritedStdio = true;
695
-
696
- const mockSpawn = vi.mocked(spawn);
697
- const mockChild = {
698
- on: vi.fn((event, callback) => {
699
- if (event === 'close') {
700
- setTimeout(() => callback(1), 10); // Failure
701
- }
702
- return mockChild;
703
- })
704
- };
705
- mockSpawn.mockReturnValue(mockChild as any);
706
-
707
- await expect(runWithDryRunSupport(command, isDryRun, {}, useInheritedStdio))
708
- .rejects.toThrow('Command "failing-command" failed with exit code 1');
709
- });
710
-
711
- test('should ignore useInheritedStdio when isDryRun is true', async () => {
712
- const command = 'test command';
713
- const isDryRun = true;
714
- const useInheritedStdio = true;
715
-
716
- const result = await runWithDryRunSupport(command, isDryRun, {}, useInheritedStdio);
717
-
718
- expect(mockLogger.info).toHaveBeenCalledWith(`DRY RUN: Would execute command: ${command}`);
719
- expect(result).toEqual({ stdout: '', stderr: '' });
720
- expect(mockExecPromise).not.toHaveBeenCalled();
721
- // spawn should not be called in dry run mode
722
- const mockSpawn = vi.mocked(spawn);
723
- expect(mockSpawn).not.toHaveBeenCalled();
724
- });
725
-
726
- test('should default useInheritedStdio to false when not provided', async () => {
727
- const command = 'test command';
728
- const isDryRun = false;
729
-
730
- mockExecPromise.mockResolvedValue({ stdout: 'output', stderr: '' });
731
-
732
- await runWithDryRunSupport(command, isDryRun);
733
-
734
- expect(mockExecPromise).toHaveBeenCalledWith(command, expect.objectContaining({
735
- encoding: 'utf8'
736
- }));
737
- // spawn should not be called when useInheritedStdio is false (default)
738
- const mockSpawn = vi.mocked(spawn);
739
- expect(mockSpawn).not.toHaveBeenCalled();
740
- });
741
- });
742
-
743
- // Additional edge cases for run function
744
- describe('child.ts - run function additional edge cases', () => {
745
- const mockExec = vi.mocked(exec);
746
- const mockPromisify = vi.mocked(promisify);
747
- const mockExecPromise = vi.fn();
748
- const mockLogger = {
749
- info: vi.fn(),
750
- error: vi.fn(),
751
- warn: vi.fn(),
752
- debug: vi.fn(),
753
- verbose: vi.fn(),
754
- silly: vi.fn()
755
- };
756
- const mockGetLogger = vi.mocked(getLogger);
757
-
758
- beforeEach(() => {
759
- vi.clearAllMocks();
760
- mockPromisify.mockReturnValue(mockExecPromise);
761
- mockGetLogger.mockReturnValue(mockLogger as any);
762
- });
763
-
764
- test('should handle null options parameter', async () => {
765
- const expectedResult = { stdout: 'test', stderr: '' };
766
- mockExecPromise.mockResolvedValue(expectedResult);
767
-
768
- const result = await run('test-command', null as any);
769
-
770
- expect(mockExecPromise).toHaveBeenCalledWith('test-command', expect.objectContaining({
771
- encoding: 'utf8'
772
- }));
773
- expect(result).toEqual(expectedResult);
774
- });
775
-
776
- test('should handle undefined options parameter explicitly', async () => {
777
- const expectedResult = { stdout: 'test', stderr: '' };
778
- mockExecPromise.mockResolvedValue(expectedResult);
779
-
780
- const result = await run('test-command', undefined);
781
-
782
- // When undefined is passed, default parameter kicks in and converts to { encoding: 'utf8' }
783
- expect(mockExecPromise).toHaveBeenCalledWith('test-command', expect.objectContaining({
784
- encoding: 'utf8'
785
- }));
786
- expect(result).toEqual(expectedResult);
787
- });
788
-
789
- test('should handle commands with newlines', async () => {
790
- const command = 'echo "line1\nline2\nline3"';
791
- const expectedResult = { stdout: 'line1\nline2\nline3', stderr: '' };
792
- mockExecPromise.mockResolvedValue(expectedResult);
793
-
794
- const result = await run(command);
795
-
796
- expect(result.stdout).toBe('line1\nline2\nline3');
797
- });
798
-
799
- test('should handle commands with tabs and special whitespace', async () => {
800
- const command = 'echo "\t\r\n\v\f"';
801
- const expectedResult = { stdout: '\t\r\n\v\f', stderr: '' };
802
- mockExecPromise.mockResolvedValue(expectedResult);
803
-
804
- const result = await run(command);
805
-
806
- expect(result.stdout).toBe('\t\r\n\v\f');
807
- });
808
-
809
- test('should handle error with custom properties', async () => {
810
- const customError = Object.assign(new Error('Custom error'), {
811
- code: 'CUSTOM_CODE',
812
- errno: -2,
813
- path: '/test/path',
814
- syscall: 'spawn'
815
- });
816
-
817
- mockExecPromise.mockRejectedValue(customError);
818
-
819
- await expect(run('custom-error-command')).rejects.toMatchObject({
820
- message: 'Custom error',
821
- code: 'CUSTOM_CODE',
822
- errno: -2,
823
- path: '/test/path',
824
- syscall: 'spawn'
825
- });
826
- });
827
- });
828
-
829
- describe('child.ts - runWithInheritedStdio function', () => {
830
- const mockSpawn = vi.mocked(spawn);
831
- const mockLogger = {
832
- info: vi.fn(),
833
- error: vi.fn(),
834
- warn: vi.fn(),
835
- debug: vi.fn(),
836
- verbose: vi.fn(),
837
- silly: vi.fn()
838
- };
839
- const mockGetLogger = vi.mocked(getLogger);
840
-
841
- beforeEach(() => {
842
- vi.clearAllMocks();
843
- mockGetLogger.mockReturnValue(mockLogger as any);
844
- });
845
-
846
- afterEach(() => {
847
- vi.restoreAllMocks();
848
- });
849
-
850
- test('should execute command successfully with inherited stdio', async () => {
851
- const mockChild = {
852
- on: vi.fn((event, callback) => {
853
- if (event === 'close') {
854
- setTimeout(() => callback(0), 10); // Simulate successful completion
855
- }
856
- return mockChild;
857
- })
858
- };
859
-
860
- mockSpawn.mockReturnValue(mockChild as any);
861
-
862
- const promise = runWithInheritedStdio('echo "hello world"');
863
-
864
- await expect(promise).resolves.toBeUndefined();
865
- expect(mockSpawn).toHaveBeenCalledWith('echo', ['"hello', 'world"'], {
866
- shell: false,
867
- stdio: 'inherit'
868
- });
869
- expect(mockLogger.verbose).toHaveBeenCalledWith('Executing command securely with inherited stdio: echo "hello world"');
870
- expect(mockLogger.verbose).toHaveBeenCalledWith('Command completed successfully with code 0');
871
- });
872
-
873
- test('should execute command with custom options', async () => {
874
- const options = {
875
- cwd: '/custom/directory',
876
- env: { NODE_ENV: 'test' }
877
- };
878
-
879
- const mockChild = {
880
- on: vi.fn((event, callback) => {
881
- if (event === 'close') {
882
- setTimeout(() => callback(0), 10);
883
- }
884
- return mockChild;
885
- })
886
- };
887
-
888
- mockSpawn.mockReturnValue(mockChild as any);
889
-
890
- await runWithInheritedStdio('npm test', options);
891
-
892
- expect(mockSpawn).toHaveBeenCalledWith('npm', ['test'], {
893
- ...options,
894
- shell: false,
895
- stdio: 'inherit'
896
- });
897
- expect(mockLogger.verbose).toHaveBeenCalledWith('Working directory: /custom/directory');
898
- });
899
-
900
- test('should handle command that fails with non-zero exit code', async () => {
901
- const mockChild = {
902
- on: vi.fn((event, callback) => {
903
- if (event === 'close') {
904
- setTimeout(() => callback(1), 10); // Simulate failure
905
- }
906
- return mockChild;
907
- })
908
- };
909
-
910
- mockSpawn.mockReturnValue(mockChild as any);
911
-
912
- await expect(runWithInheritedStdio('failing-command')).rejects.toThrow('Command "failing-command" failed with exit code 1');
913
- expect(mockLogger.error).toHaveBeenCalledWith('Command failed with exit code 1');
914
- });
915
-
916
- test('should handle spawn error', async () => {
917
- const spawnError = new Error('spawn ENOENT');
918
- const mockChild = {
919
- on: vi.fn((event, callback) => {
920
- if (event === 'error') {
921
- setTimeout(() => callback(spawnError), 10);
922
- }
923
- return mockChild;
924
- })
925
- };
926
-
927
- mockSpawn.mockReturnValue(mockChild as any);
928
-
929
- await expect(runWithInheritedStdio('invalid-command')).rejects.toThrow('spawn ENOENT');
930
- expect(mockLogger.error).toHaveBeenCalledWith('Command failed to start: spawn ENOENT');
931
- });
932
-
933
- test('should handle working directory with default process.cwd()', async () => {
934
- const mockChild = {
935
- on: vi.fn((event, callback) => {
936
- if (event === 'close') {
937
- setTimeout(() => callback(0), 10);
938
- }
939
- return mockChild;
940
- })
941
- };
942
-
943
- mockSpawn.mockReturnValue(mockChild as any);
944
-
945
- await runWithInheritedStdio('test-command');
946
-
947
- expect(mockLogger.verbose).toHaveBeenCalledWith(`Working directory: ${process.cwd()}`);
948
- });
949
-
950
- test('should handle empty command string', async () => {
951
- const mockChild = {
952
- on: vi.fn((event, callback) => {
953
- if (event === 'close') {
954
- setTimeout(() => callback(0), 10);
955
- }
956
- return mockChild;
957
- })
958
- };
959
-
960
- mockSpawn.mockReturnValue(mockChild as any);
961
-
962
- await runWithInheritedStdio('');
963
-
964
- expect(mockSpawn).toHaveBeenCalledWith('', [], {
965
- shell: false,
966
- stdio: 'inherit'
967
- });
968
- expect(mockLogger.verbose).toHaveBeenCalledWith('Executing command securely with inherited stdio: ');
969
- });
970
-
971
- test('should handle complex commands with special characters', async () => {
972
- const command = 'echo "Complex & command; with | pipes"';
973
- const mockChild = {
974
- on: vi.fn((event, callback) => {
975
- if (event === 'close') {
976
- setTimeout(() => callback(0), 10);
977
- }
978
- return mockChild;
979
- })
980
- };
981
-
982
- mockSpawn.mockReturnValue(mockChild as any);
983
-
984
- await runWithInheritedStdio(command);
985
-
986
- expect(mockSpawn).toHaveBeenCalledWith('echo', ['"Complex', '&', 'command;', 'with', '|', 'pipes"'], {
987
- shell: false,
988
- stdio: 'inherit'
989
- });
990
- });
991
-
992
- test('should handle signal termination', async () => {
993
- const mockChild = {
994
- on: vi.fn((event, callback) => {
995
- if (event === 'close') {
996
- setTimeout(() => callback(null, 'SIGTERM'), 10); // Terminated by signal
997
- }
998
- return mockChild;
999
- })
1000
- };
1001
-
1002
- mockSpawn.mockReturnValue(mockChild as any);
1003
-
1004
- await expect(runWithInheritedStdio('interruptible-command')).rejects.toThrow('Command "interruptible-command" failed with exit code null');
1005
- expect(mockLogger.error).toHaveBeenCalledWith('Command failed with exit code null');
1006
- });
1007
-
1008
- test('should preserve options immutability', async () => {
1009
- const options = {
1010
- cwd: '/test',
1011
- env: { TEST: 'value' }
1012
- };
1013
- const originalOptions = { ...options };
1014
-
1015
- const mockChild = {
1016
- on: vi.fn((event, callback) => {
1017
- if (event === 'close') {
1018
- setTimeout(() => callback(0), 10);
1019
- }
1020
- return mockChild;
1021
- })
1022
- };
1023
-
1024
- mockSpawn.mockReturnValue(mockChild as any);
1025
-
1026
- await runWithInheritedStdio('test-command', options);
1027
-
1028
- expect(options).toEqual(originalOptions);
1029
- expect(mockSpawn).toHaveBeenCalledWith('test-command', [], {
1030
- ...options,
1031
- shell: false,
1032
- stdio: 'inherit'
1033
- });
1034
- });
1035
- });