@somewhatabstract/x 0.0.1 → 0.1.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,1042 @@
1
+ import * as childProcess from "node:child_process";
2
+ import {beforeEach, describe, expect, it, vi} from "vitest";
3
+ import * as buildEnv from "../build-environment";
4
+ import {executeScript} from "../execute-script";
5
+ import type {BinInfo} from "../find-matching-bins";
6
+
7
+ // Mock child_process
8
+ vi.mock("node:child_process", () => ({
9
+ spawn: vi.fn(),
10
+ }));
11
+
12
+ // Mock build-environment
13
+ vi.mock("../build-environment", () => ({
14
+ buildEnvironment: vi.fn(),
15
+ }));
16
+
17
+ describe("executeScript", () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ it("should call buildEnvironment with workspace root and process.env", async () => {
23
+ // Arrange
24
+ const bin: BinInfo = {
25
+ packageName: "test-package",
26
+ packagePath: "/test/package",
27
+ binName: "test-bin",
28
+ binPath: "/test/package/bin/test",
29
+ };
30
+ const args = ["--arg1", "--arg2"];
31
+ const workspaceRoot = "/test/workspace";
32
+
33
+ const mockEnv = {TEST_VAR: "test"};
34
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue(mockEnv);
35
+
36
+ const mockChild = {
37
+ on: vi.fn((event, callback) => {
38
+ if (event === "exit") {
39
+ // Immediately call exit with code 0
40
+ setTimeout(() => callback(0), 0);
41
+ }
42
+ }),
43
+ };
44
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
45
+
46
+ // Act
47
+ await executeScript(bin, args, workspaceRoot);
48
+
49
+ // Assert
50
+ expect(buildEnv.buildEnvironment).toHaveBeenCalledWith(
51
+ workspaceRoot,
52
+ process.env,
53
+ );
54
+ });
55
+
56
+ it("should spawn process with bin path and args", async () => {
57
+ // Arrange
58
+ const bin: BinInfo = {
59
+ packageName: "test-package",
60
+ packagePath: "/test/package",
61
+ binName: "test-bin",
62
+ binPath: "/test/package/bin/test",
63
+ };
64
+ const args = ["--arg1", "--arg2"];
65
+ const workspaceRoot = "/test/workspace";
66
+
67
+ const mockEnv = {TEST_VAR: "test"};
68
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue(mockEnv);
69
+
70
+ const mockChild = {
71
+ on: vi.fn((event, callback) => {
72
+ if (event === "exit") {
73
+ setTimeout(() => callback(0), 0);
74
+ }
75
+ }),
76
+ };
77
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
78
+
79
+ // Act
80
+ await executeScript(bin, args, workspaceRoot);
81
+
82
+ // Assert
83
+ expect(childProcess.spawn).toHaveBeenCalledWith(
84
+ "/test/package/bin/test",
85
+ ["--arg1", "--arg2"],
86
+ expect.objectContaining({
87
+ stdio: "inherit",
88
+ env: mockEnv,
89
+ }),
90
+ );
91
+ });
92
+
93
+ it("should pass built environment to spawn", async () => {
94
+ // Arrange
95
+ const bin: BinInfo = {
96
+ packageName: "test-package",
97
+ packagePath: "/test/package",
98
+ binName: "test-bin",
99
+ binPath: "/test/package/bin/test",
100
+ };
101
+ const args: string[] = [];
102
+ const workspaceRoot = "/test/workspace";
103
+
104
+ const mockEnv = {
105
+ TEST_VAR: "test",
106
+ npm_package_name: "workspace",
107
+ };
108
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue(mockEnv);
109
+
110
+ const mockChild = {
111
+ on: vi.fn((event, callback) => {
112
+ if (event === "exit") {
113
+ setTimeout(() => callback(0), 0);
114
+ }
115
+ }),
116
+ };
117
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
118
+
119
+ // Act
120
+ await executeScript(bin, args, workspaceRoot);
121
+
122
+ // Assert
123
+ expect(childProcess.spawn).toHaveBeenCalledWith(
124
+ expect.any(String),
125
+ expect.any(Array),
126
+ expect.objectContaining({
127
+ env: mockEnv,
128
+ }),
129
+ );
130
+ });
131
+
132
+ it("should set stdio to inherit", async () => {
133
+ // Arrange
134
+ const bin: BinInfo = {
135
+ packageName: "test-package",
136
+ packagePath: "/test/package",
137
+ binName: "test-bin",
138
+ binPath: "/test/package/bin/test",
139
+ };
140
+ const args: string[] = [];
141
+ const workspaceRoot = "/test/workspace";
142
+
143
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
144
+
145
+ const mockChild = {
146
+ on: vi.fn((event, callback) => {
147
+ if (event === "exit") {
148
+ setTimeout(() => callback(0), 0);
149
+ }
150
+ }),
151
+ };
152
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
153
+
154
+ // Act
155
+ await executeScript(bin, args, workspaceRoot);
156
+
157
+ // Assert
158
+ expect(childProcess.spawn).toHaveBeenCalledWith(
159
+ expect.any(String),
160
+ expect.any(Array),
161
+ expect.objectContaining({
162
+ stdio: "inherit",
163
+ }),
164
+ );
165
+ });
166
+
167
+ it("should return exit code 0 on successful execution", async () => {
168
+ // Arrange
169
+ const bin: BinInfo = {
170
+ packageName: "test-package",
171
+ packagePath: "/test/package",
172
+ binName: "test-bin",
173
+ binPath: "/test/package/bin/test",
174
+ };
175
+ const args: string[] = [];
176
+ const workspaceRoot = "/test/workspace";
177
+
178
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
179
+
180
+ const mockChild = {
181
+ on: vi.fn((event, callback) => {
182
+ if (event === "exit") {
183
+ setTimeout(() => callback(0), 0);
184
+ }
185
+ }),
186
+ };
187
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
188
+
189
+ // Act
190
+ const exitCode = await executeScript(bin, args, workspaceRoot);
191
+
192
+ // Assert
193
+ expect(exitCode).toBe(0);
194
+ });
195
+
196
+ it("should return exit code from child process", async () => {
197
+ // Arrange
198
+ const bin: BinInfo = {
199
+ packageName: "test-package",
200
+ packagePath: "/test/package",
201
+ binName: "test-bin",
202
+ binPath: "/test/package/bin/test",
203
+ };
204
+ const args: string[] = [];
205
+ const workspaceRoot = "/test/workspace";
206
+
207
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
208
+
209
+ const mockChild = {
210
+ on: vi.fn((event, callback) => {
211
+ if (event === "exit") {
212
+ setTimeout(() => callback(42), 0);
213
+ }
214
+ }),
215
+ };
216
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
217
+
218
+ // Act
219
+ const exitCode = await executeScript(bin, args, workspaceRoot);
220
+
221
+ // Assert
222
+ expect(exitCode).toBe(42);
223
+ });
224
+
225
+ it("should return 1 when exit code is null", async () => {
226
+ // Arrange
227
+ const bin: BinInfo = {
228
+ packageName: "test-package",
229
+ packagePath: "/test/package",
230
+ binName: "test-bin",
231
+ binPath: "/test/package/bin/test",
232
+ };
233
+ const args: string[] = [];
234
+ const workspaceRoot = "/test/workspace";
235
+
236
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
237
+
238
+ const mockChild = {
239
+ on: vi.fn((event, callback) => {
240
+ if (event === "exit") {
241
+ setTimeout(() => callback(null), 0);
242
+ }
243
+ }),
244
+ };
245
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
246
+
247
+ // Act
248
+ const exitCode = await executeScript(bin, args, workspaceRoot);
249
+
250
+ // Assert
251
+ expect(exitCode).toBe(1);
252
+ });
253
+
254
+ it("should return 1 on spawn error (ENOENT)", async () => {
255
+ // Arrange
256
+ const bin: BinInfo = {
257
+ packageName: "test-package",
258
+ packagePath: "/test/package",
259
+ binName: "test-bin",
260
+ binPath: "/test/package/bin/nonexistent",
261
+ };
262
+ const args: string[] = [];
263
+ const workspaceRoot = "/test/workspace";
264
+
265
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
266
+
267
+ const mockChild = {
268
+ on: vi.fn((event, callback) => {
269
+ if (event === "error") {
270
+ setTimeout(() => {
271
+ const error: any = new Error("ENOENT");
272
+ error.code = "ENOENT";
273
+ callback(error);
274
+ }, 0);
275
+ }
276
+ }),
277
+ };
278
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
279
+
280
+ // Act
281
+ const exitCode = await executeScript(bin, args, workspaceRoot);
282
+
283
+ // Assert
284
+ expect(exitCode).toBe(1);
285
+ });
286
+
287
+ it("should return 1 on spawn error (EACCES)", async () => {
288
+ // Arrange
289
+ const bin: BinInfo = {
290
+ packageName: "test-package",
291
+ packagePath: "/test/package",
292
+ binName: "test-bin",
293
+ binPath: "/test/package/bin/nopermission",
294
+ };
295
+ const args: string[] = [];
296
+ const workspaceRoot = "/test/workspace";
297
+
298
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
299
+
300
+ const mockChild = {
301
+ on: vi.fn((event, callback) => {
302
+ if (event === "error") {
303
+ setTimeout(() => {
304
+ const error: any = new Error("EACCES");
305
+ error.code = "EACCES";
306
+ callback(error);
307
+ }, 0);
308
+ }
309
+ }),
310
+ };
311
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
312
+
313
+ // Act
314
+ const exitCode = await executeScript(bin, args, workspaceRoot);
315
+
316
+ // Assert
317
+ expect(exitCode).toBe(1);
318
+ });
319
+
320
+ it("should return 1 when process is killed by signal", async () => {
321
+ // Arrange
322
+ const bin: BinInfo = {
323
+ packageName: "test-package",
324
+ packagePath: "/test/package",
325
+ binName: "test-bin",
326
+ binPath: "/test/package/bin/test",
327
+ };
328
+ const args: string[] = [];
329
+ const workspaceRoot = "/test/workspace";
330
+
331
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
332
+
333
+ const mockChild = {
334
+ on: vi.fn((event, callback) => {
335
+ if (event === "exit") {
336
+ // Simulate SIGTERM - exit with null code and signal
337
+ setTimeout(() => callback(null, "SIGTERM"), 0);
338
+ }
339
+ }),
340
+ };
341
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
342
+
343
+ // Act
344
+ const exitCode = await executeScript(bin, args, workspaceRoot);
345
+
346
+ // Assert
347
+ expect(exitCode).toBe(1);
348
+ });
349
+
350
+ it("should return 1 when process is killed by SIGINT", async () => {
351
+ // Arrange
352
+ const bin: BinInfo = {
353
+ packageName: "test-package",
354
+ packagePath: "/test/package",
355
+ binName: "test-bin",
356
+ binPath: "/test/package/bin/test",
357
+ };
358
+ const args: string[] = [];
359
+ const workspaceRoot = "/test/workspace";
360
+
361
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
362
+
363
+ const mockChild = {
364
+ on: vi.fn((event, callback) => {
365
+ if (event === "exit") {
366
+ // Simulate SIGINT (Ctrl+C) - exit with null code and signal
367
+ setTimeout(() => callback(null, "SIGINT"), 0);
368
+ }
369
+ }),
370
+ };
371
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
372
+
373
+ // Act
374
+ const exitCode = await executeScript(bin, args, workspaceRoot);
375
+
376
+ // Assert
377
+ expect(exitCode).toBe(1);
378
+ });
379
+ });
380
+
381
+ describe("executeScript - node-executable extensions", () => {
382
+ beforeEach(() => {
383
+ vi.clearAllMocks();
384
+ });
385
+
386
+ it("should invoke .js file via node with correct args passed through", async () => {
387
+ // Arrange
388
+ const bin: BinInfo = {
389
+ packageName: "test-package",
390
+ packagePath: "/test/package",
391
+ binName: "test-bin",
392
+ binPath: "/test/package/bin/test.js",
393
+ };
394
+ const args = ["--arg1", "--arg2"];
395
+ const workspaceRoot = "/test/workspace";
396
+
397
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
398
+
399
+ const mockChild = {
400
+ on: vi.fn((event, callback) => {
401
+ if (event === "exit") {
402
+ setTimeout(() => callback(0), 0);
403
+ }
404
+ }),
405
+ };
406
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
407
+
408
+ // Act
409
+ await executeScript(bin, args, workspaceRoot);
410
+
411
+ // Assert
412
+ expect(childProcess.spawn).toHaveBeenCalledWith(
413
+ process.execPath,
414
+ ["/test/package/bin/test.js", "--arg1", "--arg2"],
415
+ expect.objectContaining({stdio: "inherit"}),
416
+ );
417
+ });
418
+
419
+ it("should invoke .mjs file via node with correct args passed through", async () => {
420
+ // Arrange
421
+ const bin: BinInfo = {
422
+ packageName: "test-package",
423
+ packagePath: "/test/package",
424
+ binName: "test-bin",
425
+ binPath: "/test/package/bin/test.mjs",
426
+ };
427
+ const args = ["--verbose"];
428
+ const workspaceRoot = "/test/workspace";
429
+
430
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
431
+
432
+ const mockChild = {
433
+ on: vi.fn((event, callback) => {
434
+ if (event === "exit") {
435
+ setTimeout(() => callback(0), 0);
436
+ }
437
+ }),
438
+ };
439
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
440
+
441
+ // Act
442
+ await executeScript(bin, args, workspaceRoot);
443
+
444
+ // Assert
445
+ expect(childProcess.spawn).toHaveBeenCalledWith(
446
+ process.execPath,
447
+ ["/test/package/bin/test.mjs", "--verbose"],
448
+ expect.objectContaining({stdio: "inherit"}),
449
+ );
450
+ });
451
+
452
+ it("should invoke .cjs file via node with correct args passed through", async () => {
453
+ // Arrange
454
+ const bin: BinInfo = {
455
+ packageName: "test-package",
456
+ packagePath: "/test/package",
457
+ binName: "test-bin",
458
+ binPath: "/test/package/bin/test.cjs",
459
+ };
460
+ const args = ["input.txt"];
461
+ const workspaceRoot = "/test/workspace";
462
+
463
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
464
+
465
+ const mockChild = {
466
+ on: vi.fn((event, callback) => {
467
+ if (event === "exit") {
468
+ setTimeout(() => callback(0), 0);
469
+ }
470
+ }),
471
+ };
472
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
473
+
474
+ // Act
475
+ await executeScript(bin, args, workspaceRoot);
476
+
477
+ // Assert
478
+ expect(childProcess.spawn).toHaveBeenCalledWith(
479
+ process.execPath,
480
+ ["/test/package/bin/test.cjs", "input.txt"],
481
+ expect.objectContaining({stdio: "inherit"}),
482
+ );
483
+ });
484
+
485
+ it("should invoke .JS file via node (case-insensitive detection)", async () => {
486
+ // Arrange
487
+ const bin: BinInfo = {
488
+ packageName: "test-package",
489
+ packagePath: "/test/package",
490
+ binName: "test-bin",
491
+ binPath: "/test/package/bin/test.JS",
492
+ };
493
+
494
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
495
+
496
+ const mockChild = {
497
+ on: vi.fn((event, callback) => {
498
+ if (event === "exit") {
499
+ setTimeout(() => callback(0), 0);
500
+ }
501
+ }),
502
+ };
503
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
504
+
505
+ // Act
506
+ await executeScript(bin, [], "/test/workspace");
507
+
508
+ // Assert: original casing preserved; node used as executable
509
+ expect(childProcess.spawn).toHaveBeenCalledWith(
510
+ process.execPath,
511
+ ["/test/package/bin/test.JS"],
512
+ expect.anything(),
513
+ );
514
+ });
515
+
516
+ it("should invoke .Js file via node (case-insensitive detection)", async () => {
517
+ // Arrange
518
+ const bin: BinInfo = {
519
+ packageName: "test-package",
520
+ packagePath: "/test/package",
521
+ binName: "test-bin",
522
+ binPath: "/test/package/bin/test.Js",
523
+ };
524
+
525
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
526
+
527
+ const mockChild = {
528
+ on: vi.fn((event, callback) => {
529
+ if (event === "exit") {
530
+ setTimeout(() => callback(0), 0);
531
+ }
532
+ }),
533
+ };
534
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
535
+
536
+ // Act
537
+ await executeScript(bin, [], "/test/workspace");
538
+
539
+ // Assert
540
+ expect(childProcess.spawn).toHaveBeenCalledWith(
541
+ process.execPath,
542
+ ["/test/package/bin/test.Js"],
543
+ expect.anything(),
544
+ );
545
+ });
546
+
547
+ it("should invoke .MJS file via node (case-insensitive detection)", async () => {
548
+ // Arrange
549
+ const bin: BinInfo = {
550
+ packageName: "test-package",
551
+ packagePath: "/test/package",
552
+ binName: "test-bin",
553
+ binPath: "/test/package/bin/test.MJS",
554
+ };
555
+
556
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
557
+
558
+ const mockChild = {
559
+ on: vi.fn((event, callback) => {
560
+ if (event === "exit") {
561
+ setTimeout(() => callback(0), 0);
562
+ }
563
+ }),
564
+ };
565
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
566
+
567
+ // Act
568
+ await executeScript(bin, [], "/test/workspace");
569
+
570
+ // Assert
571
+ expect(childProcess.spawn).toHaveBeenCalledWith(
572
+ process.execPath,
573
+ ["/test/package/bin/test.MJS"],
574
+ expect.anything(),
575
+ );
576
+ });
577
+
578
+ it("should invoke .CJS file via node (case-insensitive detection)", async () => {
579
+ // Arrange
580
+ const bin: BinInfo = {
581
+ packageName: "test-package",
582
+ packagePath: "/test/package",
583
+ binName: "test-bin",
584
+ binPath: "/test/package/bin/test.CJS",
585
+ };
586
+
587
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
588
+
589
+ const mockChild = {
590
+ on: vi.fn((event, callback) => {
591
+ if (event === "exit") {
592
+ setTimeout(() => callback(0), 0);
593
+ }
594
+ }),
595
+ };
596
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
597
+
598
+ // Act
599
+ await executeScript(bin, [], "/test/workspace");
600
+
601
+ // Assert
602
+ expect(childProcess.spawn).toHaveBeenCalledWith(
603
+ process.execPath,
604
+ ["/test/package/bin/test.CJS"],
605
+ expect.anything(),
606
+ );
607
+ });
608
+
609
+ it("should invoke .Mjs file via node (case-insensitive detection)", async () => {
610
+ // Arrange
611
+ const bin: BinInfo = {
612
+ packageName: "test-package",
613
+ packagePath: "/test/package",
614
+ binName: "test-bin",
615
+ binPath: "/test/package/bin/test.Mjs",
616
+ };
617
+
618
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
619
+
620
+ const mockChild = {
621
+ on: vi.fn((event, callback) => {
622
+ if (event === "exit") {
623
+ setTimeout(() => callback(0), 0);
624
+ }
625
+ }),
626
+ };
627
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
628
+
629
+ // Act
630
+ await executeScript(bin, [], "/test/workspace");
631
+
632
+ // Assert
633
+ expect(childProcess.spawn).toHaveBeenCalledWith(
634
+ process.execPath,
635
+ ["/test/package/bin/test.Mjs"],
636
+ expect.anything(),
637
+ );
638
+ });
639
+
640
+ it("should invoke executable bash script directly (not via node)", async () => {
641
+ // Arrange
642
+ const bin: BinInfo = {
643
+ packageName: "test-package",
644
+ packagePath: "/test/package",
645
+ binName: "test-bin",
646
+ binPath: "/test/package/bin/test.sh",
647
+ };
648
+ const args = ["--flag"];
649
+
650
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
651
+
652
+ const mockChild = {
653
+ on: vi.fn((event, callback) => {
654
+ if (event === "exit") {
655
+ setTimeout(() => callback(0), 0);
656
+ }
657
+ }),
658
+ };
659
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
660
+
661
+ // Act
662
+ await executeScript(bin, args, "/test/workspace");
663
+
664
+ // Assert: invoked directly, not via node
665
+ expect(childProcess.spawn).toHaveBeenCalledWith(
666
+ "/test/package/bin/test.sh",
667
+ ["--flag"],
668
+ expect.objectContaining({stdio: "inherit"}),
669
+ );
670
+ });
671
+
672
+ it("should invoke non-executable bash script directly (will error at OS level)", async () => {
673
+ // Arrange - a .sh without executable bit; OS will raise EACCES
674
+ const bin: BinInfo = {
675
+ packageName: "test-package",
676
+ packagePath: "/test/package",
677
+ binName: "test-bin",
678
+ binPath: "/test/package/bin/noperm.sh",
679
+ };
680
+
681
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
682
+
683
+ const mockChild = {
684
+ on: vi.fn((event, callback) => {
685
+ if (event === "error") {
686
+ setTimeout(() => {
687
+ const error: any = new Error("EACCES");
688
+ error.code = "EACCES";
689
+ callback(error);
690
+ }, 0);
691
+ }
692
+ }),
693
+ };
694
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
695
+
696
+ // Act
697
+ await executeScript(bin, [], "/test/workspace");
698
+
699
+ // Assert: invoked directly, not via node
700
+ expect(childProcess.spawn).toHaveBeenCalledWith(
701
+ "/test/package/bin/noperm.sh",
702
+ [],
703
+ expect.anything(),
704
+ );
705
+ });
706
+
707
+ it("should return 1 when non-executable bash script errors", async () => {
708
+ // Arrange
709
+ const bin: BinInfo = {
710
+ packageName: "test-package",
711
+ packagePath: "/test/package",
712
+ binName: "test-bin",
713
+ binPath: "/test/package/bin/noperm.sh",
714
+ };
715
+
716
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
717
+
718
+ const mockChild = {
719
+ on: vi.fn((event, callback) => {
720
+ if (event === "error") {
721
+ setTimeout(() => {
722
+ const error: any = new Error("EACCES");
723
+ error.code = "EACCES";
724
+ callback(error);
725
+ }, 0);
726
+ }
727
+ }),
728
+ };
729
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
730
+
731
+ // Act
732
+ const exitCode = await executeScript(bin, [], "/test/workspace");
733
+
734
+ // Assert
735
+ expect(exitCode).toBe(1);
736
+ });
737
+
738
+ it("should invoke file with no extension directly", async () => {
739
+ // Arrange
740
+ const bin: BinInfo = {
741
+ packageName: "test-package",
742
+ packagePath: "/test/package",
743
+ binName: "test-bin",
744
+ binPath: "/test/package/bin/mycli",
745
+ };
746
+
747
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
748
+
749
+ const mockChild = {
750
+ on: vi.fn((event, callback) => {
751
+ if (event === "exit") {
752
+ setTimeout(() => callback(0), 0);
753
+ }
754
+ }),
755
+ };
756
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
757
+
758
+ // Act
759
+ await executeScript(bin, [], "/test/workspace");
760
+
761
+ // Assert
762
+ expect(childProcess.spawn).toHaveBeenCalledWith(
763
+ "/test/package/bin/mycli",
764
+ [],
765
+ expect.anything(),
766
+ );
767
+ });
768
+
769
+ it("should invoke file with .js.bak extension directly (not via node)", async () => {
770
+ // Arrange - .js is in the middle, not the final extension
771
+ const bin: BinInfo = {
772
+ packageName: "test-package",
773
+ packagePath: "/test/package",
774
+ binName: "test-bin",
775
+ binPath: "/test/package/bin/script.js.bak",
776
+ };
777
+
778
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
779
+
780
+ const mockChild = {
781
+ on: vi.fn((event, callback) => {
782
+ if (event === "exit") {
783
+ setTimeout(() => callback(0), 0);
784
+ }
785
+ }),
786
+ };
787
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
788
+
789
+ // Act
790
+ await executeScript(bin, [], "/test/workspace");
791
+
792
+ // Assert: .js.bak is not a node-executable extension
793
+ expect(childProcess.spawn).toHaveBeenCalledWith(
794
+ "/test/package/bin/script.js.bak",
795
+ [],
796
+ expect.anything(),
797
+ );
798
+ });
799
+
800
+ it("should pass environment variables to node-invoked scripts", async () => {
801
+ // Arrange
802
+ const bin: BinInfo = {
803
+ packageName: "test-package",
804
+ packagePath: "/test/package",
805
+ binName: "test-bin",
806
+ binPath: "/test/package/bin/test.js",
807
+ };
808
+ const mockEnv = {MY_VAR: "my-value", npm_package_name: "workspace"};
809
+
810
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue(mockEnv);
811
+
812
+ const mockChild = {
813
+ on: vi.fn((event, callback) => {
814
+ if (event === "exit") {
815
+ setTimeout(() => callback(0), 0);
816
+ }
817
+ }),
818
+ };
819
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
820
+
821
+ // Act
822
+ await executeScript(bin, [], "/test/workspace");
823
+
824
+ // Assert
825
+ expect(childProcess.spawn).toHaveBeenCalledWith(
826
+ process.execPath,
827
+ expect.any(Array),
828
+ expect.objectContaining({env: mockEnv}),
829
+ );
830
+ });
831
+
832
+ it("should inherit stdio for node-invoked scripts", async () => {
833
+ // Arrange
834
+ const bin: BinInfo = {
835
+ packageName: "test-package",
836
+ packagePath: "/test/package",
837
+ binName: "test-bin",
838
+ binPath: "/test/package/bin/test.mjs",
839
+ };
840
+
841
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
842
+
843
+ const mockChild = {
844
+ on: vi.fn((event, callback) => {
845
+ if (event === "exit") {
846
+ setTimeout(() => callback(0), 0);
847
+ }
848
+ }),
849
+ };
850
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
851
+
852
+ // Act
853
+ await executeScript(bin, [], "/test/workspace");
854
+
855
+ // Assert
856
+ expect(childProcess.spawn).toHaveBeenCalledWith(
857
+ process.execPath,
858
+ expect.any(Array),
859
+ expect.objectContaining({stdio: "inherit"}),
860
+ );
861
+ });
862
+
863
+ it("should pass exit code through from node-invoked scripts", async () => {
864
+ // Arrange
865
+ const bin: BinInfo = {
866
+ packageName: "test-package",
867
+ packagePath: "/test/package",
868
+ binName: "test-bin",
869
+ binPath: "/test/package/bin/test.js",
870
+ };
871
+
872
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
873
+
874
+ const mockChild = {
875
+ on: vi.fn((event, callback) => {
876
+ if (event === "exit") {
877
+ setTimeout(() => callback(42), 0);
878
+ }
879
+ }),
880
+ };
881
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
882
+
883
+ // Act
884
+ const exitCode = await executeScript(bin, [], "/test/workspace");
885
+
886
+ // Assert
887
+ expect(exitCode).toBe(42);
888
+ });
889
+
890
+ it("should return 1 on spawn error (ENOENT) for node-invoked scripts", async () => {
891
+ // Arrange
892
+ const bin: BinInfo = {
893
+ packageName: "test-package",
894
+ packagePath: "/test/package",
895
+ binName: "test-bin",
896
+ binPath: "/test/package/bin/missing.js",
897
+ };
898
+
899
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
900
+
901
+ const mockChild = {
902
+ on: vi.fn((event, callback) => {
903
+ if (event === "error") {
904
+ setTimeout(() => {
905
+ const error: any = new Error("ENOENT");
906
+ error.code = "ENOENT";
907
+ callback(error);
908
+ }, 0);
909
+ }
910
+ }),
911
+ };
912
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
913
+
914
+ // Act
915
+ const exitCode = await executeScript(bin, [], "/test/workspace");
916
+
917
+ // Assert
918
+ expect(exitCode).toBe(1);
919
+ });
920
+
921
+ it("should return 1 when node-invoked script is killed by SIGTERM", async () => {
922
+ // Arrange
923
+ const bin: BinInfo = {
924
+ packageName: "test-package",
925
+ packagePath: "/test/package",
926
+ binName: "test-bin",
927
+ binPath: "/test/package/bin/test.js",
928
+ };
929
+
930
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
931
+
932
+ const mockChild = {
933
+ on: vi.fn((event, callback) => {
934
+ if (event === "exit") {
935
+ setTimeout(() => callback(null, "SIGTERM"), 0);
936
+ }
937
+ }),
938
+ };
939
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
940
+
941
+ // Act
942
+ const exitCode = await executeScript(bin, [], "/test/workspace");
943
+
944
+ // Assert
945
+ expect(exitCode).toBe(1);
946
+ });
947
+
948
+ it("should return 1 when node-invoked script is killed by SIGINT", async () => {
949
+ // Arrange
950
+ const bin: BinInfo = {
951
+ packageName: "test-package",
952
+ packagePath: "/test/package",
953
+ binName: "test-bin",
954
+ binPath: "/test/package/bin/test.mjs",
955
+ };
956
+
957
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
958
+
959
+ const mockChild = {
960
+ on: vi.fn((event, callback) => {
961
+ if (event === "exit") {
962
+ setTimeout(() => callback(null, "SIGINT"), 0);
963
+ }
964
+ }),
965
+ };
966
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
967
+
968
+ // Act
969
+ const exitCode = await executeScript(bin, [], "/test/workspace");
970
+
971
+ // Assert
972
+ expect(exitCode).toBe(1);
973
+ });
974
+
975
+ it("should preserve arguments with spaces for node-invoked scripts", async () => {
976
+ // Arrange
977
+ const bin: BinInfo = {
978
+ packageName: "test-package",
979
+ packagePath: "/test/package",
980
+ binName: "test-bin",
981
+ binPath: "/test/package/bin/test.js",
982
+ };
983
+ const args = ["file with spaces.txt", "--message=hello world"];
984
+
985
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
986
+
987
+ const mockChild = {
988
+ on: vi.fn((event, callback) => {
989
+ if (event === "exit") {
990
+ setTimeout(() => callback(0), 0);
991
+ }
992
+ }),
993
+ };
994
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
995
+
996
+ // Act
997
+ await executeScript(bin, args, "/test/workspace");
998
+
999
+ // Assert
1000
+ expect(childProcess.spawn).toHaveBeenCalledWith(
1001
+ process.execPath,
1002
+ [
1003
+ "/test/package/bin/test.js",
1004
+ "file with spaces.txt",
1005
+ "--message=hello world",
1006
+ ],
1007
+ expect.anything(),
1008
+ );
1009
+ });
1010
+
1011
+ it("should pass multiple arguments correctly to node-invoked scripts", async () => {
1012
+ // Arrange
1013
+ const bin: BinInfo = {
1014
+ packageName: "test-package",
1015
+ packagePath: "/test/package",
1016
+ binName: "test-bin",
1017
+ binPath: "/test/package/bin/test.cjs",
1018
+ };
1019
+ const args = ["--a", "--b", "--c", "value"];
1020
+
1021
+ vi.mocked(buildEnv.buildEnvironment).mockResolvedValue({});
1022
+
1023
+ const mockChild = {
1024
+ on: vi.fn((event, callback) => {
1025
+ if (event === "exit") {
1026
+ setTimeout(() => callback(0), 0);
1027
+ }
1028
+ }),
1029
+ };
1030
+ vi.mocked(childProcess.spawn).mockReturnValue(mockChild as any);
1031
+
1032
+ // Act
1033
+ await executeScript(bin, args, "/test/workspace");
1034
+
1035
+ // Assert
1036
+ expect(childProcess.spawn).toHaveBeenCalledWith(
1037
+ process.execPath,
1038
+ ["/test/package/bin/test.cjs", "--a", "--b", "--c", "value"],
1039
+ expect.anything(),
1040
+ );
1041
+ });
1042
+ });