@bash0816/claude-code 2.1.220 → 2.1.223-1

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,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const test = require('node:test');
4
+ const { mock } = require('node:test');
4
5
  const assert = require('node:assert/strict');
5
6
  const crypto = require('crypto');
6
7
  const child_process = require('child_process');
@@ -191,12 +192,32 @@ test('print path does not defer cleanup to exit', () => {
191
192
  test('bootstrap path defers cleanup to exit', () => {
192
193
  const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
193
194
 
194
- assert.equal(bootstrapBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), false);
195
- assert.equal(bootstrapBlock.includes('setTimeout(resolve, printWaitMs)'), false);
196
195
  assert.equal(bootstrapBlock.includes('process.once(\'exit\''), true);
197
196
  assert.equal(bootstrapBlock.includes('process.removeListener(\'uncaughtException\''), true);
198
197
  });
199
198
 
199
+ test('bootstrap branch exports CLAUDE_TERMUX_PRINT_MODE before invoking node', () => {
200
+ const bootstrapShellRegion = extractFunction(
201
+ script,
202
+ 'export CLAUDE_TERMUX_TUI="${_tui}"',
203
+ 'cat <<\'NODE\' > "$_bootstrap"',
204
+ );
205
+ assert.equal(bootstrapShellRegion.includes('export CLAUDE_TERMUX_PRINT_MODE="${_pf}"'), true);
206
+ });
207
+
208
+ test('helper and bootstrap wait for print flush, including the RequestedExit path', () => {
209
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
210
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
211
+
212
+ assert.equal(helperBlock.includes('async function waitForPrintFlush()'), true);
213
+ assert.equal(helperBlock.includes('await waitForPrintFlush();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
214
+ assert.equal(helperBlock.includes('process.exitCode = error.code;\n await waitForPrintFlush();\n return;'), true);
215
+
216
+ assert.equal(bootstrapBlock.includes('async function waitForPrintFlushIfNeeded()'), true);
217
+ assert.equal(bootstrapBlock.includes('await waitForPrintFlushIfNeeded();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
218
+ assert.equal(bootstrapBlock.includes('process.exitCode = error.code;\n await waitForPrintFlushIfNeeded();\n return;'), true);
219
+ });
220
+
200
221
  test('entry extraction uses a process-unique filename', () => {
201
222
  const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
202
223
  const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
@@ -280,9 +301,36 @@ test('cleanupStaleEntryFiles removes only stale extracted files for the same off
280
301
  function buildSyntheticBundleSource() {
281
302
  const typeofBun = Array.from({ length: 6 }, () => 'typeof Bun').join('; ');
282
303
  const bunProps = Array.from({ length: 37 }, (_, index) => `Bun.p${index}`).join('; ');
283
- return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`;
304
+ return `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
284
305
  }
285
306
 
307
+ test('rewriteNativeChunkSource disables the bg-pty-host factory', () => {
308
+ process.env.CURRENT_CLAUDE_VERSION = '2.1.198';
309
+ try {
310
+ const { rewriteNativeChunkSource } = loadHelperApi();
311
+ const patched = rewriteNativeChunkSource(buildSyntheticBundleSource());
312
+ const start = patched.indexOf('function dYs(){');
313
+ assert.notEqual(start, -1);
314
+ let depth = 0;
315
+ let end = -1;
316
+ for (let index = start; index < patched.length; index += 1) {
317
+ if (patched[index] === '{') depth += 1;
318
+ if (patched[index] === '}') {
319
+ depth -= 1;
320
+ if (depth === 0) {
321
+ end = index + 1;
322
+ break;
323
+ }
324
+ }
325
+ }
326
+ assert.notEqual(end, -1);
327
+ const rewrittenFactory = eval('(' + patched.slice(start, end) + ')');
328
+ assert.equal(rewrittenFactory(), undefined);
329
+ } finally {
330
+ delete process.env.CURRENT_CLAUDE_VERSION;
331
+ }
332
+ });
333
+
286
334
  test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.198)', () => {
287
335
  process.env.CURRENT_CLAUDE_VERSION = '2.1.198';
288
336
  try {
@@ -308,7 +356,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
308
356
  const { rewriteNativeChunkSource } = loadHelperApi();
309
357
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
310
358
  const bunProps = Array.from({ length: 40 }, (_, index) => `Bun.p${index}`).join('; ');
311
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`;
359
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
312
360
  const patched = rewriteNativeChunkSource(source);
313
361
 
314
362
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -329,7 +377,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
329
377
  const { rewriteNativeChunkSource } = loadHelperApi();
330
378
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
331
379
  const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; ');
332
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0 }`;
380
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
333
381
  const patched = rewriteNativeChunkSource(source);
334
382
 
335
383
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -350,7 +398,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
350
398
  const { rewriteNativeChunkSource } = loadHelperApi();
351
399
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
352
400
  const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; ');
353
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
401
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
354
402
  const patched = rewriteNativeChunkSource(source);
355
403
 
356
404
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -371,7 +419,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
371
419
  const { rewriteNativeChunkSource } = loadHelperApi();
372
420
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
373
421
  const bunProps = Array.from({ length: 38 }, (_, index) => `Bun.p${index}`).join('; ');
374
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
422
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
375
423
  const patched = rewriteNativeChunkSource(source);
376
424
 
377
425
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -392,7 +440,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi
392
440
  const { rewriteNativeChunkSource } = loadHelperApi();
393
441
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
394
442
  const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; ');
395
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
443
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
396
444
 
397
445
  assert.throws(
398
446
  () => rewriteNativeChunkSource(source),
@@ -409,7 +457,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
409
457
  const { rewriteNativeChunkSource } = loadHelperApi();
410
458
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
411
459
  const bunProps = Array.from({ length: 39 }, (_, index) => `Bun.p${index}`).join('; ');
412
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
460
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
413
461
  const patched = rewriteNativeChunkSource(source);
414
462
 
415
463
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -430,7 +478,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi
430
478
  const { rewriteNativeChunkSource } = loadHelperApi();
431
479
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
432
480
  const bunProps = Array.from({ length: 38 }, (_, index) => `Bun.p${index}`).join('; ');
433
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
481
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
434
482
 
435
483
  assert.throws(
436
484
  () => rewriteNativeChunkSource(source),
@@ -447,7 +495,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
447
495
  const { rewriteNativeChunkSource } = loadHelperApi();
448
496
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
449
497
  const bunProps = Array.from({ length: 40 }, (_, index) => `Bun.p${index}`).join('; ');
450
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
498
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
451
499
  const patched = rewriteNativeChunkSource(source);
452
500
 
453
501
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -467,7 +515,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi
467
515
  const { rewriteNativeChunkSource } = loadHelperApi();
468
516
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
469
517
  const bunProps = Array.from({ length: 39 }, (_, index) => `Bun.p${index}`).join('; ');
470
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
518
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
471
519
 
472
520
  assert.throws(
473
521
  () => rewriteNativeChunkSource(source),
@@ -484,7 +532,7 @@ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.
484
532
  const { rewriteNativeChunkSource } = loadHelperApi();
485
533
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
486
534
  const bunProps = Array.from({ length: 42 }, (_, index) => `Bun.p${index}`).join('; ');
487
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
535
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
488
536
  const patched = rewriteNativeChunkSource(source);
489
537
 
490
538
  assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
@@ -504,7 +552,7 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi
504
552
  const { rewriteNativeChunkSource } = loadHelperApi();
505
553
  const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
506
554
  const bunProps = Array.from({ length: 41 }, (_, index) => `Bun.p${index}`).join('; ');
507
- const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0 }`;
555
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
508
556
 
509
557
  assert.throws(
510
558
  () => rewriteNativeChunkSource(source),
@@ -515,6 +563,43 @@ test('rewriteNativeChunkSource rejects stale Bun property access count for versi
515
563
  }
516
564
  });
517
565
 
566
+ test('rewriteNativeChunkSource rewrites the synthetic bundle slice (version 2.1.223)', () => {
567
+ process.env.CURRENT_CLAUDE_VERSION = '2.1.223';
568
+ try {
569
+ const { rewriteNativeChunkSource } = loadHelperApi();
570
+ const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
571
+ const bunProps = Array.from({ length: 43 }, (_, index) => `Bun.p${index}`).join('; ');
572
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
573
+ const patched = rewriteNativeChunkSource(source);
574
+
575
+ assert.match(patched, /var __claudeBun = globalThis\.__claudeBunShim;/);
576
+ assert.match(patched, /typeof __claudeBun/);
577
+ assert.match(patched, /typeof globalThis\.__claudeBun/);
578
+ assert.match(patched, /globalThis\.__claudeBun/);
579
+ assert.match(patched, /__claudeBun\./);
580
+ assert.equal((patched.match(/__claudeBun\./g) || []).length, 43);
581
+ } finally {
582
+ delete process.env.CURRENT_CLAUDE_VERSION;
583
+ }
584
+ });
585
+
586
+ test('rewriteNativeChunkSource rejects stale Bun property access count for version 2.1.223', () => {
587
+ process.env.CURRENT_CLAUDE_VERSION = '2.1.223';
588
+ try {
589
+ const { rewriteNativeChunkSource } = loadHelperApi();
590
+ const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
591
+ const bunProps = Array.from({ length: 42 }, (_, index) => `Bun.p${index}`).join('; ');
592
+ const source = `function(exports, require, module, __filename, __dirname) { ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps}; npmInstallDeprecated:!0; npmInstallDeprecated:!0; function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}} }`;
593
+
594
+ assert.throws(
595
+ () => rewriteNativeChunkSource(source),
596
+ /unexpected Bun property access count 42/,
597
+ );
598
+ } finally {
599
+ delete process.env.CURRENT_CLAUDE_VERSION;
600
+ }
601
+ });
602
+
518
603
  test('rewriteNativeChunkSource rejects unexpected replacement counts', () => {
519
604
  const { rewriteNativeChunkSource } = loadHelperApi();
520
605
  const source = buildSyntheticBundleSource().replace('Bun.p30;', '');
@@ -544,3 +629,920 @@ test('tarball contents match the workspace runner and test file', { skip: !fs.ex
544
629
  test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is never forced (regression guard)', () => {
545
630
  assert.equal(script.includes('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'), false);
546
631
  });
632
+
633
+ function buildScenarioFixtureSource() {
634
+ const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
635
+ const bunProps = Array.from({ length: 42 }, (_, i) => `Bun.p${i}`).join('; ');
636
+ return `function(exports, require, module, __filename, __dirname) {
637
+ ${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps};
638
+ npmInstallDeprecated:!0; npmInstallDeprecated:!0;
639
+ function dYs(){return(e,t,r)=>{let{cmd:n,prefixArgs:o}=ox({pinToCurrentBinary:!0}),i=[n,...o,"--bg-pty-host",r.ptySock];return i}}
640
+ const scenario = process.env.TEST_SCENARIO;
641
+ if (scenario === 'sync-exit') { process.stdout.write('ok'); process.exit(0); return; }
642
+ if (scenario === 'async-exit') {
643
+ setTimeout(() => { process.stdout.write('ok'); }, 100).unref();
644
+ return;
645
+ }
646
+ if (scenario === 'self-sigkill-fallback-string') {
647
+ try {
648
+ process.exit(17);
649
+ } catch (e) {
650
+ process.kill(process.pid, 'SIGKILL');
651
+ }
652
+ return;
653
+ }
654
+ if (scenario === 'self-sigkill-fallback-numeric') {
655
+ try {
656
+ process.exit(17);
657
+ } catch (e) {
658
+ process.kill(process.pid, 9);
659
+ }
660
+ return;
661
+ }
662
+ if (scenario === 'self-sigkill-fallback-no-code') {
663
+ try {
664
+ process.exit();
665
+ } catch (e) {
666
+ process.kill(process.pid, 'SIGKILL');
667
+ }
668
+ return;
669
+ }
670
+ if (scenario === 'other-process-kill') {
671
+ const cp = require('child_process');
672
+ const child = cp.spawn('node', ['-e', 'setTimeout(() => {}, 5000)']);
673
+ const childPid = child.pid;
674
+ process.kill(childPid, 0);
675
+ child.kill();
676
+ process.stdout.write('ok');
677
+ return;
678
+ }
679
+ if (scenario === 'stream-json-result') {
680
+ process.stdout.write('{"type":"init"}\\n');
681
+ const msg = '{"type":"result","data":"test"}\\n';
682
+ process.stdout.write(msg, undefined, () => {});
683
+ return;
684
+ }
685
+ if (scenario === 'stream-json-multibyte-split') {
686
+ // Split a UTF-8 multi-byte character across write calls
687
+ // 'あ' is 3 bytes in UTF-8: e3 81 82
688
+ const buf = Buffer.from('あ', 'utf8');
689
+ // Split the 3-byte character: first byte in one write, remaining in another
690
+ const jsonLine = '{"type":"result"}\\n';
691
+ const part1 = Buffer.concat([Buffer.from(jsonLine), buf.slice(0, 1)]);
692
+ const part2 = Buffer.concat([buf.slice(1)]);
693
+ process.stdout.write(part1);
694
+ process.stdout.write(part2, undefined, () => {});
695
+ return;
696
+ }
697
+ if (scenario === 'stream-json-timeout') {
698
+ process.stdout.write('{"type":"init"}\\n');
699
+ setTimeout(() => {}, 5000);
700
+ return;
701
+ }
702
+ if (scenario === 'stream-json-requested-exit-then-result') {
703
+ process.stdout.write('{"type":"result"}\\n');
704
+ process.exit(0);
705
+ return;
706
+ }
707
+ process.stdout.write('ok');
708
+ }`;
709
+ }
710
+
711
+ function runScenario({ printMode, stdinInherit, scenario, extraArgs }) {
712
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stdin-test-'));
713
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
714
+ const fixtureSource = buildScenarioFixtureSource();
715
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
716
+ const entryJsOffset = 0;
717
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
718
+ const workdir = path.join(tmpBase, 'workdir');
719
+ fs.mkdirSync(workdir, { recursive: true });
720
+
721
+ const env = {
722
+ ...process.env,
723
+ SOURCE_BIN: sourceBin,
724
+ WORKDIR: workdir,
725
+ ENTRY_JS_OFFSET: String(entryJsOffset),
726
+ ENTRY_END_OFFSET: String(entryEndOffset),
727
+ CURRENT_CLAUDE_VERSION: '2.1.220',
728
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
729
+ MAGI_ENV: '1',
730
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
731
+ TMPDIR: tmpBase,
732
+ TEST_SCENARIO: scenario,
733
+ };
734
+ if (stdinInherit) env.CLAUDE_TERMUX_STDIN = 'inherit';
735
+ else delete env.CLAUDE_TERMUX_STDIN;
736
+
737
+ const args = printMode ? ['-p', 'x', ...(extraArgs || [])] : [];
738
+ const start = Date.now();
739
+ const result = child_process.spawnSync('sh', [scriptPath, ...args], {
740
+ env,
741
+ input: 'test input\n',
742
+ encoding: 'utf8',
743
+ timeout: 10000,
744
+ });
745
+ const elapsedMs = Date.now() - start;
746
+ return { ...result, elapsedMs, tmpBase };
747
+ }
748
+
749
+ test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', () => {
750
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
751
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
752
+
753
+ // Check helper branch
754
+ const helperOriginalKillIdx = helperBlock.indexOf('const originalKill = process.kill;');
755
+ assert.ok(helperOriginalKillIdx > -1, 'helper: missing originalKill declaration');
756
+
757
+ const helperProcessKillIdx = helperBlock.indexOf('process.kill = (pid, signal) => {', helperOriginalKillIdx);
758
+ assert.ok(helperProcessKillIdx > helperOriginalKillIdx, 'helper: process.kill override must come after originalKill declaration');
759
+
760
+ const helperRestoreIdx = helperBlock.indexOf('process.kill = originalKill;', helperProcessKillIdx);
761
+ assert.ok(helperRestoreIdx > helperProcessKillIdx, 'helper: process.kill restoration must come after override');
762
+
763
+ // Verify SIGKILL check within the override
764
+ const helperKillOverrideEnd = helperBlock.indexOf('};', helperProcessKillIdx);
765
+ const helperKillOverride = helperBlock.slice(helperProcessKillIdx, helperKillOverrideEnd);
766
+ assert.ok(helperKillOverride.includes('signal === \'SIGKILL\''), 'helper: process.kill must check for SIGKILL string');
767
+ assert.ok(helperKillOverride.includes('signal === 9'), 'helper: process.kill must check for SIGKILL numeric value');
768
+ assert.ok(helperKillOverride.includes('throw new RequestedExit'), 'helper: process.kill must throw RequestedExit for self SIGKILL');
769
+
770
+ // Check bootstrap branch
771
+ const bootstrapOriginalKillIdx = bootstrapBlock.indexOf('const originalKill = process.kill;');
772
+ assert.ok(bootstrapOriginalKillIdx > -1, 'bootstrap: missing originalKill declaration');
773
+
774
+ const bootstrapProcessKillIdx = bootstrapBlock.indexOf('process.kill = (pid, signal) => {', bootstrapOriginalKillIdx);
775
+ assert.ok(bootstrapProcessKillIdx > bootstrapOriginalKillIdx, 'bootstrap: process.kill override must come after originalKill declaration');
776
+
777
+ const bootstrapRestoreIdx = bootstrapBlock.indexOf('process.kill = originalKill;', bootstrapProcessKillIdx);
778
+ assert.ok(bootstrapRestoreIdx > bootstrapProcessKillIdx, 'bootstrap: process.kill restoration must come after override');
779
+
780
+ // Verify SIGKILL check within the override
781
+ const bootstrapKillOverrideEnd = bootstrapBlock.indexOf('};', bootstrapProcessKillIdx);
782
+ const bootstrapKillOverride = bootstrapBlock.slice(bootstrapProcessKillIdx, bootstrapKillOverrideEnd);
783
+ assert.ok(bootstrapKillOverride.includes('signal === \'SIGKILL\''), 'bootstrap: process.kill must check for SIGKILL string');
784
+ assert.ok(bootstrapKillOverride.includes('signal === 9'), 'bootstrap: process.kill must check for SIGKILL numeric value');
785
+ assert.ok(bootstrapKillOverride.includes('throw new RequestedExit'), 'bootstrap: process.kill must throw RequestedExit for self SIGKILL');
786
+ });
787
+
788
+ test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => {
789
+ for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
790
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario });
791
+ try {
792
+ assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
793
+ assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
794
+ } finally {
795
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
796
+ }
797
+ }
798
+ });
799
+
800
+ test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/async-exit all produce output', () => {
801
+ for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
802
+ const r = runScenario({ printMode: true, stdinInherit: true, scenario });
803
+ try {
804
+ assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
805
+ assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
806
+ if (scenario === 'async-exit') {
807
+ assert.ok(r.elapsedMs >= 250, `expected wait >= 250ms, got ${r.elapsedMs}ms`);
808
+ }
809
+ } finally {
810
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
811
+ }
812
+ }
813
+ });
814
+
815
+ test('helper branch intercepts self-directed SIGKILL (string signal) and exits with proper code', () => {
816
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-string' });
817
+ try {
818
+ assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
819
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
820
+ } finally {
821
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
822
+ }
823
+ });
824
+
825
+ test('helper branch intercepts self-directed SIGKILL (numeric signal 9) and exits with proper code', () => {
826
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-numeric' });
827
+ try {
828
+ assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
829
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
830
+ } finally {
831
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
832
+ }
833
+ });
834
+
835
+ test('helper branch intercepts self-directed SIGKILL with process.exit() (no code) and defaults to 0', () => {
836
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-no-code' });
837
+ try {
838
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
839
+ assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
840
+ } finally {
841
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
842
+ }
843
+ });
844
+
845
+ test('helper branch allows process.kill to other processes (signal 0, passthrough)', () => {
846
+ const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'other-process-kill' });
847
+ try {
848
+ assert.ok((r.stdout || '').includes('ok'), `expected ok output, got stdout=${r.stdout} stderr=${r.stderr}`);
849
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
850
+ } finally {
851
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
852
+ }
853
+ });
854
+
855
+ test('isStreamJsonPrintMode detects print flag and stream-json format (case 1: -p + format + value)', () => {
856
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
857
+ const fnSource = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
858
+ const context = vm.createContext({ module: { exports: {} } });
859
+ vm.runInContext(`${fnSource}\nmodule.exports = isStreamJsonPrintMode;`, context);
860
+ const isStreamJsonPrintMode = context.module.exports;
861
+
862
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'stream-json']), true, 'case 1');
863
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format=stream-json']), true, 'case 2');
864
+ assert.equal(isStreamJsonPrintMode(['--print', '--output-format=stream-json']), true, 'case 3');
865
+ assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'json']), false, 'case 4');
866
+ assert.equal(isStreamJsonPrintMode(['-p']), false, 'case 5');
867
+ assert.equal(isStreamJsonPrintMode(['--output-format=stream-json']), false, 'case 6');
868
+ assert.equal(isStreamJsonPrintMode(['-p', '--', '--output-format=stream-json']), false, 'case 7');
869
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format=stream-json', '--', 'extra']), true, 'case 8');
870
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format', '--', 'stream-json']), false, 'case 9');
871
+ assert.equal(isStreamJsonPrintMode(['-p', '--output-format']), false, 'case 10');
872
+ assert.equal(isStreamJsonPrintMode([]), false, 'case 11');
873
+ });
874
+
875
+ test('isStreamJsonPrintMode is identical in helper and bootstrap', () => {
876
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
877
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
878
+
879
+ const helperFn = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
880
+ const bootstrapFn = extractFunction(bootstrapBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
881
+
882
+ assert.equal(helperFn, bootstrapFn, 'isStreamJsonPrintMode must be identical in both heredocs');
883
+ });
884
+
885
+ test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on NaN', () => {
886
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
887
+ try {
888
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
889
+ const fixtureSource = buildScenarioFixtureSource();
890
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
891
+ const entryJsOffset = 0;
892
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
893
+ const workdir = path.join(tmpBase, 'workdir');
894
+ fs.mkdirSync(workdir, { recursive: true });
895
+
896
+ const env = {
897
+ ...process.env,
898
+ SOURCE_BIN: sourceBin,
899
+ WORKDIR: workdir,
900
+ ENTRY_JS_OFFSET: String(entryJsOffset),
901
+ ENTRY_END_OFFSET: String(entryEndOffset),
902
+ CURRENT_CLAUDE_VERSION: '2.1.220',
903
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
904
+ MAGI_ENV: '1',
905
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
906
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: 'invalid',
907
+ TMPDIR: tmpBase,
908
+ TEST_SCENARIO: 'stream-json-result',
909
+ };
910
+ delete env.CLAUDE_TERMUX_STDIN;
911
+
912
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
913
+ env,
914
+ input: 'test input\n',
915
+ encoding: 'utf8',
916
+ timeout: 10000,
917
+ });
918
+ assert.equal(result.status, 0, `expected successful exit with invalid timeout fallback, got status=${result.status} stderr=${result.stderr}`);
919
+ } finally {
920
+ fs.rmSync(tmpBase, { recursive: true, force: true });
921
+ }
922
+ });
923
+
924
+ test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on zero', () => {
925
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
926
+ try {
927
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
928
+ const fixtureSource = buildScenarioFixtureSource();
929
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
930
+ const entryJsOffset = 0;
931
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
932
+ const workdir = path.join(tmpBase, 'workdir');
933
+ fs.mkdirSync(workdir, { recursive: true });
934
+
935
+ const env = {
936
+ ...process.env,
937
+ SOURCE_BIN: sourceBin,
938
+ WORKDIR: workdir,
939
+ ENTRY_JS_OFFSET: String(entryJsOffset),
940
+ ENTRY_END_OFFSET: String(entryEndOffset),
941
+ CURRENT_CLAUDE_VERSION: '2.1.220',
942
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
943
+ MAGI_ENV: '1',
944
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
945
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '0',
946
+ TMPDIR: tmpBase,
947
+ TEST_SCENARIO: 'stream-json-result',
948
+ };
949
+ delete env.CLAUDE_TERMUX_STDIN;
950
+
951
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
952
+ env,
953
+ input: 'test input\n',
954
+ encoding: 'utf8',
955
+ timeout: 10000,
956
+ });
957
+ assert.equal(result.status, 0, `expected successful exit with zero timeout fallback, got status=${result.status} stderr=${result.stderr}`);
958
+ } finally {
959
+ fs.rmSync(tmpBase, { recursive: true, force: true });
960
+ }
961
+ });
962
+
963
+ test('helper branch (CLI -p, no stdin inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
964
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-'));
965
+ try {
966
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
967
+ const fixtureSource = buildScenarioFixtureSource();
968
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
969
+ const entryJsOffset = 0;
970
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
971
+ const workdir = path.join(tmpBase, 'workdir');
972
+ fs.mkdirSync(workdir, { recursive: true });
973
+
974
+ const env = {
975
+ ...process.env,
976
+ SOURCE_BIN: sourceBin,
977
+ WORKDIR: workdir,
978
+ ENTRY_JS_OFFSET: String(entryJsOffset),
979
+ ENTRY_END_OFFSET: String(entryEndOffset),
980
+ CURRENT_CLAUDE_VERSION: '2.1.220',
981
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
982
+ MAGI_ENV: '1',
983
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
984
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
985
+ TMPDIR: tmpBase,
986
+ TEST_SCENARIO: 'stream-json-result',
987
+ };
988
+ delete env.CLAUDE_TERMUX_STDIN;
989
+
990
+ const start = Date.now();
991
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
992
+ env,
993
+ input: 'test input\n',
994
+ encoding: 'utf8',
995
+ timeout: 20000,
996
+ });
997
+ const elapsedMs = Date.now() - start;
998
+
999
+ assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
1000
+ assert.ok(
1001
+ elapsedMs < 3000,
1002
+ `expected process to exit quickly after result detected, but elapsed=${elapsedMs}ms (should be < 3000ms with 10000ms timeout). This indicates the timeout timer was not cleared.`
1003
+ );
1004
+ } finally {
1005
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1006
+ }
1007
+ });
1008
+
1009
+ test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
1010
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-bootstrap-'));
1011
+ try {
1012
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
1013
+ const fixtureSource = buildScenarioFixtureSource();
1014
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
1015
+ const entryJsOffset = 0;
1016
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
1017
+ const workdir = path.join(tmpBase, 'workdir');
1018
+ fs.mkdirSync(workdir, { recursive: true });
1019
+
1020
+ const env = {
1021
+ ...process.env,
1022
+ SOURCE_BIN: sourceBin,
1023
+ WORKDIR: workdir,
1024
+ ENTRY_JS_OFFSET: String(entryJsOffset),
1025
+ ENTRY_END_OFFSET: String(entryEndOffset),
1026
+ CURRENT_CLAUDE_VERSION: '2.1.220',
1027
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
1028
+ MAGI_ENV: '1',
1029
+ CLAUDE_TERMUX_STDIN: 'inherit',
1030
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
1031
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
1032
+ TMPDIR: tmpBase,
1033
+ TEST_SCENARIO: 'stream-json-result',
1034
+ };
1035
+
1036
+ const start = Date.now();
1037
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
1038
+ env,
1039
+ input: 'test input\n',
1040
+ encoding: 'utf8',
1041
+ timeout: 20000,
1042
+ });
1043
+ const elapsedMs = Date.now() - start;
1044
+
1045
+ assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
1046
+ assert.ok(
1047
+ elapsedMs < 3000,
1048
+ `expected process to exit quickly after result detected, but elapsed=${elapsedMs}ms (should be < 3000ms with 10000ms timeout). This indicates the timeout timer was not cleared.`
1049
+ );
1050
+ } finally {
1051
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1052
+ }
1053
+ });
1054
+
1055
+ test('helper and bootstrap installStreamJsonTerminalWatcher helpers stay identical', () => {
1056
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1057
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1058
+
1059
+ const helperWatcher = extractFunction(
1060
+ helperBlock,
1061
+ 'function installStreamJsonTerminalWatcher() {',
1062
+ '\n function forceTimeoutExit',
1063
+ );
1064
+ const bootstrapWatcher = extractFunction(
1065
+ bootstrapBlock,
1066
+ 'function installStreamJsonTerminalWatcher() {',
1067
+ '\n function forceTimeoutExit',
1068
+ );
1069
+
1070
+ assert.equal(helperWatcher, bootstrapWatcher, 'installStreamJsonTerminalWatcher must be identical in both heredocs');
1071
+ });
1072
+
1073
+ test('helper and bootstrap forceTimeoutExit helpers stay identical', () => {
1074
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1075
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1076
+
1077
+ const helperForceExit = extractFunction(
1078
+ helperBlock,
1079
+ 'function forceTimeoutExit(exitCode) {',
1080
+ '\n async function waitForPrintFlush',
1081
+ );
1082
+ const bootstrapForceExit = extractFunction(
1083
+ bootstrapBlock,
1084
+ 'function forceTimeoutExit(exitCode) {',
1085
+ '\n async function waitForPrintFlushIfNeeded',
1086
+ );
1087
+
1088
+ assert.equal(helperForceExit, bootstrapForceExit, 'forceTimeoutExit must be identical in both heredocs');
1089
+ });
1090
+
1091
+ test('installStreamJsonTerminalWatcher restores process.stdout.write own property state', () => {
1092
+ // Test with the real process.stdout to verify own property handling
1093
+ const hadOwnPropertyBefore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1094
+ const descriptorBefore = hadOwnPropertyBefore ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
1095
+
1096
+ // Get the watcher function from helper
1097
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1098
+ const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
1099
+
1100
+ const context = vm.createContext({
1101
+ module: { exports: {} },
1102
+ process,
1103
+ Object,
1104
+ Buffer,
1105
+ require: (id) => {
1106
+ if (id === 'string_decoder') return require('string_decoder');
1107
+ throw new Error('require not available');
1108
+ },
1109
+ });
1110
+
1111
+ vm.runInContext(`
1112
+ ${watcherSource}
1113
+ module.exports = installStreamJsonTerminalWatcher;
1114
+ `, context);
1115
+
1116
+ const installStreamJsonTerminalWatcher = context.module.exports;
1117
+
1118
+ try {
1119
+ // Test case 1: Normal case where write is not an own property
1120
+ {
1121
+ const watcher = installStreamJsonTerminalWatcher();
1122
+ const hadOwnPropertyAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1123
+ assert.equal(hadOwnPropertyAfterInstall, true, 'after install: process.stdout.write should be own property');
1124
+
1125
+ // Restore watcher
1126
+ watcher.restore();
1127
+ const hadOwnPropertyAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1128
+ assert.equal(hadOwnPropertyAfterRestore, hadOwnPropertyBefore, 'after restore: own property state should match initial');
1129
+
1130
+ // Verify restore is idempotent
1131
+ watcher.restore();
1132
+ const hadOwnPropertyAfterSecondRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1133
+ assert.equal(hadOwnPropertyAfterSecondRestore, hadOwnPropertyBefore, 'second restore should also maintain initial state');
1134
+ }
1135
+
1136
+ // Test case 2: When write is an own property before installation
1137
+ {
1138
+ const testDescriptor = {
1139
+ value: function testWrite() { return true; },
1140
+ writable: true,
1141
+ configurable: true,
1142
+ enumerable: false,
1143
+ };
1144
+ Object.defineProperty(process.stdout, 'write', testDescriptor);
1145
+
1146
+ const watcher = installStreamJsonTerminalWatcher();
1147
+ const hadOwnAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1148
+ assert.equal(hadOwnAfterInstall, true, 'test case 2: after install should have own property');
1149
+
1150
+ watcher.restore();
1151
+ const hadOwnAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
1152
+ assert.equal(hadOwnAfterRestore, true, 'test case 2: after restore should still have own property');
1153
+
1154
+ const restoredDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'write');
1155
+ assert.equal(typeof restoredDescriptor.value, 'function', 'test case 2: restored value should be a function');
1156
+ assert.equal(restoredDescriptor.configurable, true, 'test case 2: restored configurable should match');
1157
+ }
1158
+ } finally {
1159
+ // Ensure stdout.write is fully restored to original state
1160
+ if (hadOwnPropertyBefore && descriptorBefore) {
1161
+ Object.defineProperty(process.stdout, 'write', descriptorBefore);
1162
+ } else if (Object.prototype.hasOwnProperty.call(process.stdout, 'write')) {
1163
+ delete process.stdout.write;
1164
+ }
1165
+ }
1166
+ });
1167
+
1168
+ test('helper branch stream-json result detection (single write)', () => {
1169
+ const r = runScenario({
1170
+ printMode: true,
1171
+ stdinInherit: false,
1172
+ scenario: 'stream-json-result',
1173
+ extraArgs: ['--output-format=stream-json'],
1174
+ });
1175
+ try {
1176
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1177
+ // Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
1178
+ assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
1179
+ } finally {
1180
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1181
+ }
1182
+ });
1183
+
1184
+ test('bootstrap branch stream-json result detection (single write)', () => {
1185
+ const r = runScenario({
1186
+ printMode: true,
1187
+ stdinInherit: true,
1188
+ scenario: 'stream-json-result',
1189
+ extraArgs: ['--output-format=stream-json'],
1190
+ });
1191
+ try {
1192
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1193
+ // Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
1194
+ assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
1195
+ } finally {
1196
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1197
+ }
1198
+ });
1199
+
1200
+ test('helper branch stream-json multibyte character split handling', () => {
1201
+ const r = runScenario({
1202
+ printMode: true,
1203
+ stdinInherit: false,
1204
+ scenario: 'stream-json-multibyte-split',
1205
+ extraArgs: ['--output-format=stream-json'],
1206
+ });
1207
+ try {
1208
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1209
+ assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
1210
+ } finally {
1211
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1212
+ }
1213
+ });
1214
+
1215
+ test('bootstrap branch stream-json multibyte character split handling', () => {
1216
+ const r = runScenario({
1217
+ printMode: true,
1218
+ stdinInherit: true,
1219
+ scenario: 'stream-json-multibyte-split',
1220
+ extraArgs: ['--output-format=stream-json'],
1221
+ });
1222
+ try {
1223
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1224
+ assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
1225
+ } finally {
1226
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1227
+ }
1228
+ });
1229
+
1230
+ test('helper branch stream-json timeout triggers exit with status 1', () => {
1231
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
1232
+ try {
1233
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
1234
+ const fixtureSource = buildScenarioFixtureSource();
1235
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
1236
+ const entryJsOffset = 0;
1237
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
1238
+ const workdir = path.join(tmpBase, 'workdir');
1239
+ fs.mkdirSync(workdir, { recursive: true });
1240
+
1241
+ const env = {
1242
+ ...process.env,
1243
+ SOURCE_BIN: sourceBin,
1244
+ WORKDIR: workdir,
1245
+ ENTRY_JS_OFFSET: String(entryJsOffset),
1246
+ ENTRY_END_OFFSET: String(entryEndOffset),
1247
+ CURRENT_CLAUDE_VERSION: '2.1.220',
1248
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
1249
+ MAGI_ENV: '1',
1250
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
1251
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
1252
+ TMPDIR: tmpBase,
1253
+ TEST_SCENARIO: 'stream-json-timeout',
1254
+ };
1255
+ delete env.CLAUDE_TERMUX_STDIN;
1256
+
1257
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
1258
+ env,
1259
+ input: 'test input\n',
1260
+ encoding: 'utf8',
1261
+ timeout: 5000,
1262
+ });
1263
+
1264
+ assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
1265
+ const entries = fs.readdirSync(workdir, { withFileTypes: true });
1266
+ const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
1267
+ assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
1268
+ } finally {
1269
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1270
+ }
1271
+ });
1272
+
1273
+ test('bootstrap branch stream-json timeout triggers exit with status 1', () => {
1274
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
1275
+ try {
1276
+ const sourceBin = path.join(tmpBase, 'fake-source.js');
1277
+ const fixtureSource = buildScenarioFixtureSource();
1278
+ fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
1279
+ const entryJsOffset = 0;
1280
+ const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
1281
+ const workdir = path.join(tmpBase, 'workdir');
1282
+ fs.mkdirSync(workdir, { recursive: true });
1283
+
1284
+ const env = {
1285
+ ...process.env,
1286
+ SOURCE_BIN: sourceBin,
1287
+ WORKDIR: workdir,
1288
+ ENTRY_JS_OFFSET: String(entryJsOffset),
1289
+ ENTRY_END_OFFSET: String(entryEndOffset),
1290
+ CURRENT_CLAUDE_VERSION: '2.1.220',
1291
+ CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
1292
+ MAGI_ENV: '1',
1293
+ CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
1294
+ CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
1295
+ CLAUDE_TERMUX_STDIN: 'inherit',
1296
+ TMPDIR: tmpBase,
1297
+ TEST_SCENARIO: 'stream-json-timeout',
1298
+ };
1299
+
1300
+ const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
1301
+ env,
1302
+ input: 'test input\n',
1303
+ encoding: 'utf8',
1304
+ timeout: 5000,
1305
+ });
1306
+
1307
+ assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
1308
+ const entries = fs.readdirSync(workdir, { withFileTypes: true });
1309
+ const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
1310
+ assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
1311
+ } finally {
1312
+ fs.rmSync(tmpBase, { recursive: true, force: true });
1313
+ }
1314
+ });
1315
+
1316
+ test('helper branch stream-json requested exit after result', () => {
1317
+ const r = runScenario({
1318
+ printMode: true,
1319
+ stdinInherit: false,
1320
+ scenario: 'stream-json-requested-exit-then-result',
1321
+ extraArgs: ['--output-format=stream-json'],
1322
+ });
1323
+ try {
1324
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1325
+ } finally {
1326
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1327
+ }
1328
+ });
1329
+
1330
+ test('bootstrap branch stream-json requested exit after result', () => {
1331
+ const r = runScenario({
1332
+ printMode: true,
1333
+ stdinInherit: true,
1334
+ scenario: 'stream-json-requested-exit-then-result',
1335
+ extraArgs: ['--output-format=stream-json'],
1336
+ });
1337
+ try {
1338
+ assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
1339
+ } finally {
1340
+ fs.rmSync(r.tmpBase, { recursive: true, force: true });
1341
+ }
1342
+ });
1343
+
1344
+ test('installStreamJsonTerminalWatcher waits for write callback before completing result', async () => {
1345
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1346
+ const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
1347
+
1348
+ // Create a dedicated mock stdout object instead of modifying the real one
1349
+ let callbackFired = false;
1350
+ const mockStdout = Object.create(Object.getPrototypeOf(process.stdout));
1351
+
1352
+ // Copy necessary properties
1353
+ Object.defineProperty(mockStdout, 'write', {
1354
+ value: function(chunk, encoding, callback) {
1355
+ if (typeof encoding === 'function') {
1356
+ callback = encoding;
1357
+ encoding = undefined;
1358
+ }
1359
+ if (callback) {
1360
+ // Defer callback to next microtask
1361
+ setImmediate(() => {
1362
+ callbackFired = true;
1363
+ callback();
1364
+ });
1365
+ }
1366
+ return true;
1367
+ },
1368
+ writable: true,
1369
+ configurable: true,
1370
+ });
1371
+
1372
+ const context = vm.createContext({
1373
+ module: { exports: {} },
1374
+ process: { stdout: mockStdout },
1375
+ Object,
1376
+ Buffer,
1377
+ require: (id) => {
1378
+ if (id === 'string_decoder') return require('string_decoder');
1379
+ throw new Error('require not available');
1380
+ },
1381
+ });
1382
+
1383
+ vm.runInContext(`
1384
+ ${watcherSource}
1385
+ module.exports = installStreamJsonTerminalWatcher;
1386
+ `, context);
1387
+
1388
+ const installStreamJsonTerminalWatcher = context.module.exports;
1389
+ const watcher = installStreamJsonTerminalWatcher();
1390
+
1391
+ // Simulate a write with result JSON
1392
+ const resultJson = '{"type":"result","data":"test"}\n';
1393
+ mockStdout.write(resultJson, 'utf8');
1394
+
1395
+ // Get the promise before callback fires
1396
+ const resultPromise = watcher.waitForResult();
1397
+
1398
+ // Give time for callback to fire
1399
+ await new Promise(resolve => setTimeout(resolve, 50));
1400
+
1401
+ // Promise should now be resolved
1402
+ await resultPromise;
1403
+ assert.ok(callbackFired, 'callback should have been fired');
1404
+
1405
+ watcher.restore();
1406
+ });
1407
+
1408
+ function extractShimSource(block) {
1409
+ const startMarker = 'const _realChild = require(\'child_process\');';
1410
+ const endMarker = 'Object.assign(globalThis.__claudeBunShim, globalThis.Bun);';
1411
+ const start = block.indexOf(startMarker);
1412
+ assert.notEqual(start, -1, 'missing Bun shim start');
1413
+ const end = block.indexOf(endMarker, start);
1414
+ assert.notEqual(end, -1, 'missing Bun shim end');
1415
+ return block.slice(start, end + endMarker.length);
1416
+ }
1417
+
1418
+ function loadBunShim(source) {
1419
+ const context = vm.createContext({
1420
+ stringWidth: () => 0,
1421
+ wrapAnsi: value => value,
1422
+ stripANSI: value => value,
1423
+ stableHash: () => 0,
1424
+ __claudeYaml: {},
1425
+ Buffer,
1426
+ require,
1427
+ });
1428
+ context.__claudeBunShim = {};
1429
+ context.__claudeYaml = {};
1430
+ context.Bun = {};
1431
+ context.module = { exports: {} };
1432
+ vm.runInContext(`${source}\nmodule.exports = globalThis.__claudeBunShim;`, context);
1433
+ return context.module.exports;
1434
+ }
1435
+
1436
+ test('helper and bootstrap Bun shim source is identical', () => {
1437
+ const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1438
+ const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1439
+ assert.equal(extractShimSource(helperBlock), extractShimSource(bootstrapBlock));
1440
+ });
1441
+
1442
+ test('Bun.file always throws ENOENT', () => {
1443
+ for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) {
1444
+ const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1445
+ const Bun = loadBunShim(extractShimSource(block));
1446
+ assert.throws(() => Bun.file('/some/path'), error =>
1447
+ error.code === 'ENOENT' && error.errno === -2);
1448
+ }
1449
+ });
1450
+
1451
+ test('Bun.spawn supports top-level and array stdio forms', async () => {
1452
+ const block = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1453
+ const Bun = loadBunShim(extractShimSource(block));
1454
+ const topLevel = Bun.spawn(['echo', 'hello'], { stdout: 'pipe', stderr: 'ignore' });
1455
+ assert.equal(await topLevel.stdout.text(), 'hello\n');
1456
+ assert.equal(await topLevel.exited, 0);
1457
+ const arrayForm = Bun.spawn(['echo', 'hello'], { stdio: ['ignore', 'pipe', 'ignore'] });
1458
+ assert.equal(await arrayForm.stdout.text(), 'hello\n');
1459
+ assert.equal(await arrayForm.exited, 0);
1460
+ });
1461
+
1462
+ test('Bun.spawn forwards options, preserves numeric fds, and delegates child controls', () => {
1463
+ const block = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1464
+ const calls = [];
1465
+ const handlers = {};
1466
+ const child = {
1467
+ pid: 123,
1468
+ stdout: null,
1469
+ on(event, handler) { handlers[event] = handler; return this; },
1470
+ unref() { calls.push(['unref']); },
1471
+ kill(signal) { calls.push(['kill', signal]); },
1472
+ };
1473
+ mock.method(child_process, 'spawn', (...args) => {
1474
+ calls.push(args);
1475
+ return child;
1476
+ });
1477
+ try {
1478
+ const Bun = loadBunShim(extractShimSource(block));
1479
+ const result = Bun.spawn(['cmd', 'arg'], {
1480
+ detached: true,
1481
+ argv0: 'argv0-value',
1482
+ cwd: '/tmp/work',
1483
+ env: { TEST: 'yes' },
1484
+ stdio: [0, 1, 2],
1485
+ });
1486
+ assert.deepEqual(JSON.parse(JSON.stringify(calls[0])), [
1487
+ 'cmd',
1488
+ ['arg'],
1489
+ {
1490
+ stdio: [0, 1, 2],
1491
+ cwd: '/tmp/work',
1492
+ env: { TEST: 'yes' },
1493
+ detached: true,
1494
+ argv0: 'argv0-value',
1495
+ },
1496
+ ]);
1497
+ result.unref();
1498
+ result.kill('SIGTERM');
1499
+ assert.deepEqual(JSON.parse(JSON.stringify(calls.slice(1))), [['unref'], ['kill', 'SIGTERM']]);
1500
+ } finally {
1501
+ mock.restoreAll();
1502
+ }
1503
+ });
1504
+
1505
+ test('Bun.spawn prefers stdio array over top-level stdio options', () => {
1506
+ for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) {
1507
+ const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1508
+ const calls = [];
1509
+ mock.method(child_process, 'spawn', (...args) => {
1510
+ calls.push(args);
1511
+ return { pid: 123, stdout: null, on() { return this; } };
1512
+ });
1513
+ try {
1514
+ const Bun = loadBunShim(extractShimSource(block));
1515
+ Bun.spawn(['cmd'], {
1516
+ stdio: [0, 1, 2],
1517
+ stdin: 'pipe',
1518
+ stdout: 'pipe',
1519
+ stderr: 'pipe',
1520
+ });
1521
+ assert.deepEqual(JSON.parse(JSON.stringify(calls[0][2])), {
1522
+ stdio: [0, 1, 2],
1523
+ detached: false,
1524
+ });
1525
+ } finally {
1526
+ mock.restoreAll();
1527
+ }
1528
+ }
1529
+ });
1530
+
1531
+ test('Bun.spawn forwards top-level stdin in the first stdio position', () => {
1532
+ for (const blockMarker of ['cat <<\'NODE\' > "$_helper"', 'cat <<\'NODE\' > "$_bootstrap"']) {
1533
+ const block = extractBlock(blockMarker, '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
1534
+ const calls = [];
1535
+ mock.method(child_process, 'spawn', (...args) => {
1536
+ calls.push(args);
1537
+ return { pid: 123, stdout: null, on() { return this; } };
1538
+ });
1539
+ try {
1540
+ const Bun = loadBunShim(extractShimSource(block));
1541
+ Bun.spawn(['cmd'], { stdin: 'inherit', stdout: 'pipe', stderr: 'ignore' });
1542
+ assert.equal(calls[0][2].stdio[0], 'inherit');
1543
+ assert.deepEqual(JSON.parse(JSON.stringify(calls[0][2].stdio)), ['inherit', 'pipe', 'ignore']);
1544
+ } finally {
1545
+ mock.restoreAll();
1546
+ }
1547
+ }
1548
+ });