@bash0816/claude-code 2.1.220 → 2.1.223
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
|
+
});
|