@bash0816/claude-code 2.1.220 → 2.1.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -580,6 +580,42 @@
|
|
|
580
580
|
"entry_end_offset": 265457701,
|
|
581
581
|
"tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
|
|
582
582
|
"tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
|
|
583
|
+
"status": "termux_verified"
|
|
584
|
+
},
|
|
585
|
+
"2.1.220-1": {
|
|
586
|
+
"wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
|
|
587
|
+
"native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
|
|
588
|
+
"entry_js_offset": 243831156,
|
|
589
|
+
"entry_end_offset": 265457701,
|
|
590
|
+
"tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
|
|
591
|
+
"tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
|
|
592
|
+
"status": "offset_discovered"
|
|
593
|
+
},
|
|
594
|
+
"2.1.220-2": {
|
|
595
|
+
"wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
|
|
596
|
+
"native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
|
|
597
|
+
"entry_js_offset": 243831156,
|
|
598
|
+
"entry_end_offset": 265457701,
|
|
599
|
+
"tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
|
|
600
|
+
"tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
|
|
601
|
+
"status": "termux_verified"
|
|
602
|
+
},
|
|
603
|
+
"2.1.220-3": {
|
|
604
|
+
"wrapper_spec": "@anthropic-ai/claude-code@2.1.220",
|
|
605
|
+
"native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.220",
|
|
606
|
+
"entry_js_offset": 243831156,
|
|
607
|
+
"entry_end_offset": 265457701,
|
|
608
|
+
"tarball_integrity": "sha512-VHFI8mKruIntKn7eq81sbyS19/KWmQcmJQsS/C+j9M/E+w0s4UytgsL7DADPjBE/GByNiKoRtLYDMntCjRlOdA==",
|
|
609
|
+
"tarball_sha256": "e38454d73576a08a2e707f26539d73fc9ef33e890228ca5c58a2bbe810ac884d",
|
|
610
|
+
"status": "offset_discovered"
|
|
611
|
+
},
|
|
612
|
+
"2.1.222": {
|
|
613
|
+
"wrapper_spec": "@anthropic-ai/claude-code@2.1.222",
|
|
614
|
+
"native_spec": "@anthropic-ai/claude-code-linux-arm64@2.1.222",
|
|
615
|
+
"entry_js_offset": 256895476,
|
|
616
|
+
"entry_end_offset": 279849039,
|
|
617
|
+
"tarball_integrity": "sha512-EXSediF1ujcqQeElbXdCEe+uW3NUQAOjn1xHHAgvwzY08nzJeePjagn9+YS6bmhHxBgDKwlGu7pL8Kv7EpiFtg==",
|
|
618
|
+
"tarball_sha256": "1bb3c8364d652dd08a15856ee27c6600ecd1e45360243154fa846f240ce7a1df",
|
|
583
619
|
"status": "offset_discovered"
|
|
584
620
|
}
|
|
585
621
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 1,
|
|
3
3
|
"package_name": "@bash0816/claude-code",
|
|
4
|
-
"latest_audited_version": "2.1.
|
|
5
|
-
"latest_candidate_version": "2.1.
|
|
6
|
-
"previous_stable_version": "2.1.
|
|
4
|
+
"latest_audited_version": "2.1.220-2",
|
|
5
|
+
"latest_candidate_version": "2.1.222",
|
|
6
|
+
"previous_stable_version": "2.1.220",
|
|
7
7
|
"stable_pinned_version": "2.1.193",
|
|
8
8
|
"manifest_url": "https://raw.githubusercontent.com/bash0816/ClaudeCode-Termux/main/config/claude-termux-release-manifest.json"
|
|
9
9
|
}
|
|
@@ -79,6 +79,22 @@ const entryJsOffset = Number(process.env.ENTRY_JS_OFFSET);
|
|
|
79
79
|
const entryEndOffset = Number(process.env.ENTRY_END_OFFSET);
|
|
80
80
|
const argv = process.argv.slice(2);
|
|
81
81
|
|
|
82
|
+
function isStreamJsonPrintMode(argv) {
|
|
83
|
+
const dashDashIndex = argv.indexOf('--');
|
|
84
|
+
const ownArgs = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
85
|
+
const hasPrintFlag = ownArgs.includes('-p') || ownArgs.includes('--print');
|
|
86
|
+
let hasStreamJsonFormat = false;
|
|
87
|
+
for (let i = 0; i < ownArgs.length; i++) {
|
|
88
|
+
const tok = ownArgs[i];
|
|
89
|
+
if (tok === '--output-format=stream-json') { hasStreamJsonFormat = true; break; }
|
|
90
|
+
if (tok === '--output-format' && ownArgs[i + 1] === 'stream-json') {
|
|
91
|
+
hasStreamJsonFormat = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return hasPrintFlag && hasStreamJsonFormat;
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
class RequestedExit extends Error {
|
|
83
99
|
constructor(code) {
|
|
84
100
|
super(`process.exit ${code}`);
|
|
@@ -623,10 +639,12 @@ async function main() {
|
|
|
623
639
|
|
|
624
640
|
const originalArgv = process.argv.slice();
|
|
625
641
|
const originalExit = process.exit;
|
|
642
|
+
const originalKill = process.kill;
|
|
626
643
|
const originalBun = process.versions.bun;
|
|
627
644
|
const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
|
|
628
645
|
const originalGlobalBun = globalThis.Bun;
|
|
629
646
|
const asyncErrors = [];
|
|
647
|
+
let streamJsonWatcher = null;
|
|
630
648
|
|
|
631
649
|
globalThis.__claudeYaml = createYamlShim();
|
|
632
650
|
if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
|
|
@@ -636,6 +654,100 @@ async function main() {
|
|
|
636
654
|
function onAsyncError(error) {
|
|
637
655
|
asyncErrors.push(error);
|
|
638
656
|
}
|
|
657
|
+
const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
|
|
658
|
+
|
|
659
|
+
function installStreamJsonTerminalWatcher() {
|
|
660
|
+
const hadOwnWrite = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
661
|
+
const originalWriteDescriptor = hadOwnWrite ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
|
|
662
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
663
|
+
const { StringDecoder } = require('string_decoder');
|
|
664
|
+
const decoder = new StringDecoder('utf8');
|
|
665
|
+
let lineBuffer = '';
|
|
666
|
+
let resultPromiseResolve = null;
|
|
667
|
+
let foundResult = false;
|
|
668
|
+
let restored = false;
|
|
669
|
+
|
|
670
|
+
process.stdout.write = function wrappedWrite(chunk, encoding, callback) {
|
|
671
|
+
let cb = callback;
|
|
672
|
+
let enc = encoding;
|
|
673
|
+
if (typeof encoding === 'function') {
|
|
674
|
+
cb = encoding;
|
|
675
|
+
enc = undefined;
|
|
676
|
+
}
|
|
677
|
+
const combinedCallback = (err) => {
|
|
678
|
+
if (!err && !foundResult) {
|
|
679
|
+
const chunkStr = typeof chunk === 'string'
|
|
680
|
+
? decoder.write(Buffer.from(chunk, typeof enc === 'string' ? enc : 'utf8'))
|
|
681
|
+
: decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
682
|
+
lineBuffer += chunkStr;
|
|
683
|
+
const lines = lineBuffer.split('\n');
|
|
684
|
+
lineBuffer = lines[lines.length - 1];
|
|
685
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(lines[i]);
|
|
688
|
+
if (parsed && parsed.type === 'result') {
|
|
689
|
+
foundResult = true;
|
|
690
|
+
if (typeof resultPromiseResolve === 'function') resultPromiseResolve();
|
|
691
|
+
}
|
|
692
|
+
} catch {}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
if (typeof cb === 'function') cb(err);
|
|
696
|
+
};
|
|
697
|
+
return originalWrite(chunk, enc, combinedCallback);
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
waitForResult() {
|
|
702
|
+
if (foundResult) return Promise.resolve();
|
|
703
|
+
return new Promise((resolve, reject) => {
|
|
704
|
+
resultPromiseResolve = resolve;
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
restore() {
|
|
708
|
+
if (restored) return;
|
|
709
|
+
restored = true;
|
|
710
|
+
if (hadOwnWrite) {
|
|
711
|
+
Object.defineProperty(process.stdout, 'write', originalWriteDescriptor);
|
|
712
|
+
} else {
|
|
713
|
+
delete process.stdout.write;
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function forceTimeoutExit(exitCode) {
|
|
720
|
+
try { if (streamJsonWatcher) streamJsonWatcher.restore(); } catch {}
|
|
721
|
+
if (extractedFile) {
|
|
722
|
+
try { fs.rmSync(extractedFile, { force: true }); } catch {}
|
|
723
|
+
}
|
|
724
|
+
originalExit(exitCode);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function waitForPrintFlush() {
|
|
728
|
+
if (isStreamJsonPrintMode(argv) && streamJsonWatcher) {
|
|
729
|
+
let timedOut = false;
|
|
730
|
+
const rawResultTimeoutMs = Number(process.env.CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS);
|
|
731
|
+
const resultTimeoutMs = (Number.isFinite(rawResultTimeoutMs) && rawResultTimeoutMs > 0) ? rawResultTimeoutMs : 300000;
|
|
732
|
+
let timeoutHandle;
|
|
733
|
+
const timeoutPromise = new Promise(resolve => {
|
|
734
|
+
timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, resultTimeoutMs);
|
|
735
|
+
});
|
|
736
|
+
try {
|
|
737
|
+
await Promise.race([streamJsonWatcher.waitForResult(), timeoutPromise]);
|
|
738
|
+
} finally {
|
|
739
|
+
clearTimeout(timeoutHandle);
|
|
740
|
+
}
|
|
741
|
+
if (timedOut) {
|
|
742
|
+
forceTimeoutExit(1);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
|
|
748
|
+
await new Promise(resolve => setTimeout(resolve, printWaitMs));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
639
751
|
|
|
640
752
|
try {
|
|
641
753
|
process.once('uncaughtException', onAsyncError);
|
|
@@ -694,21 +806,31 @@ async function main() {
|
|
|
694
806
|
globalThis.__claudeBun = globalThis.__claudeBunShim;
|
|
695
807
|
globalThis.Bun = globalThis.__claudeBunShim;
|
|
696
808
|
process.argv = ['node', extractedFile, ...argv];
|
|
809
|
+
let lastExitAttemptCode = 0;
|
|
697
810
|
process.exit = code => {
|
|
698
|
-
|
|
811
|
+
lastExitAttemptCode = code ?? 0;
|
|
812
|
+
throw new RequestedExit(lastExitAttemptCode);
|
|
813
|
+
};
|
|
814
|
+
process.kill = (pid, signal) => {
|
|
815
|
+
if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) {
|
|
816
|
+
throw new RequestedExit(lastExitAttemptCode);
|
|
817
|
+
}
|
|
818
|
+
return originalKill.call(process, pid, signal);
|
|
699
819
|
};
|
|
700
820
|
|
|
701
|
-
|
|
821
|
+
if (isStreamJsonPrintMode(argv)) {
|
|
822
|
+
streamJsonWatcher = installStreamJsonTerminalWatcher();
|
|
823
|
+
}
|
|
824
|
+
|
|
702
825
|
const moduleLike = { exports: {} };
|
|
703
826
|
const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
|
|
704
827
|
if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
|
|
705
|
-
|
|
706
|
-
await new Promise(resolve => setTimeout(resolve, printWaitMs));
|
|
707
|
-
}
|
|
828
|
+
await waitForPrintFlush();
|
|
708
829
|
if (asyncErrors.length > 0) throw asyncErrors[0];
|
|
709
830
|
} catch (error) {
|
|
710
831
|
if (error instanceof RequestedExit) {
|
|
711
832
|
process.exitCode = error.code;
|
|
833
|
+
await waitForPrintFlush();
|
|
712
834
|
return;
|
|
713
835
|
}
|
|
714
836
|
throw error;
|
|
@@ -722,6 +844,10 @@ async function main() {
|
|
|
722
844
|
}
|
|
723
845
|
process.argv = originalArgv;
|
|
724
846
|
process.exit = originalExit;
|
|
847
|
+
process.kill = originalKill;
|
|
848
|
+
if (streamJsonWatcher) {
|
|
849
|
+
try { streamJsonWatcher.restore(); } catch {}
|
|
850
|
+
}
|
|
725
851
|
try {
|
|
726
852
|
if (originalBun === undefined) {
|
|
727
853
|
delete process.versions.bun;
|
|
@@ -758,6 +884,7 @@ else
|
|
|
758
884
|
_bootstrap=$(mktemp "${TERMUX_TMPDIR}/claude-bootstrap.XXXXXX.js")
|
|
759
885
|
trap 'rm -f "$_bootstrap"' EXIT HUP INT TERM
|
|
760
886
|
export CLAUDE_TERMUX_TUI="${_tui}"
|
|
887
|
+
export CLAUDE_TERMUX_PRINT_MODE="${_pf}"
|
|
761
888
|
cat <<'NODE' > "$_bootstrap"
|
|
762
889
|
const fs = require('fs');
|
|
763
890
|
const path = require('path');
|
|
@@ -772,6 +899,22 @@ const entryJsOffset = Number(process.env.ENTRY_JS_OFFSET);
|
|
|
772
899
|
const entryEndOffset = Number(process.env.ENTRY_END_OFFSET);
|
|
773
900
|
const argv = process.argv.slice(2);
|
|
774
901
|
|
|
902
|
+
function isStreamJsonPrintMode(argv) {
|
|
903
|
+
const dashDashIndex = argv.indexOf('--');
|
|
904
|
+
const ownArgs = dashDashIndex === -1 ? argv : argv.slice(0, dashDashIndex);
|
|
905
|
+
const hasPrintFlag = ownArgs.includes('-p') || ownArgs.includes('--print');
|
|
906
|
+
let hasStreamJsonFormat = false;
|
|
907
|
+
for (let i = 0; i < ownArgs.length; i++) {
|
|
908
|
+
const tok = ownArgs[i];
|
|
909
|
+
if (tok === '--output-format=stream-json') { hasStreamJsonFormat = true; break; }
|
|
910
|
+
if (tok === '--output-format' && ownArgs[i + 1] === 'stream-json') {
|
|
911
|
+
hasStreamJsonFormat = true;
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return hasPrintFlag && hasStreamJsonFormat;
|
|
916
|
+
}
|
|
917
|
+
|
|
775
918
|
class RequestedExit extends Error {
|
|
776
919
|
constructor(code) {
|
|
777
920
|
super(`process.exit ${code}`);
|
|
@@ -1316,10 +1459,12 @@ async function main() {
|
|
|
1316
1459
|
|
|
1317
1460
|
const originalArgv = process.argv.slice();
|
|
1318
1461
|
const originalExit = process.exit;
|
|
1462
|
+
const originalKill = process.kill;
|
|
1319
1463
|
const originalBun = process.versions.bun;
|
|
1320
1464
|
const hadGlobalBun = Object.prototype.hasOwnProperty.call(globalThis, 'Bun');
|
|
1321
1465
|
const originalGlobalBun = globalThis.Bun;
|
|
1322
1466
|
const asyncErrors = [];
|
|
1467
|
+
let streamJsonWatcher = null;
|
|
1323
1468
|
|
|
1324
1469
|
globalThis.__claudeYaml = createYamlShim();
|
|
1325
1470
|
if (!globalThis.__claudeBunShim || typeof globalThis.__claudeBunShim !== 'object') {
|
|
@@ -1330,6 +1475,101 @@ async function main() {
|
|
|
1330
1475
|
asyncErrors.push(error);
|
|
1331
1476
|
}
|
|
1332
1477
|
|
|
1478
|
+
function installStreamJsonTerminalWatcher() {
|
|
1479
|
+
const hadOwnWrite = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1480
|
+
const originalWriteDescriptor = hadOwnWrite ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
|
|
1481
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
1482
|
+
const { StringDecoder } = require('string_decoder');
|
|
1483
|
+
const decoder = new StringDecoder('utf8');
|
|
1484
|
+
let lineBuffer = '';
|
|
1485
|
+
let resultPromiseResolve = null;
|
|
1486
|
+
let foundResult = false;
|
|
1487
|
+
let restored = false;
|
|
1488
|
+
|
|
1489
|
+
process.stdout.write = function wrappedWrite(chunk, encoding, callback) {
|
|
1490
|
+
let cb = callback;
|
|
1491
|
+
let enc = encoding;
|
|
1492
|
+
if (typeof encoding === 'function') {
|
|
1493
|
+
cb = encoding;
|
|
1494
|
+
enc = undefined;
|
|
1495
|
+
}
|
|
1496
|
+
const combinedCallback = (err) => {
|
|
1497
|
+
if (!err && !foundResult) {
|
|
1498
|
+
const chunkStr = typeof chunk === 'string'
|
|
1499
|
+
? decoder.write(Buffer.from(chunk, typeof enc === 'string' ? enc : 'utf8'))
|
|
1500
|
+
: decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1501
|
+
lineBuffer += chunkStr;
|
|
1502
|
+
const lines = lineBuffer.split('\n');
|
|
1503
|
+
lineBuffer = lines[lines.length - 1];
|
|
1504
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
1505
|
+
try {
|
|
1506
|
+
const parsed = JSON.parse(lines[i]);
|
|
1507
|
+
if (parsed && parsed.type === 'result') {
|
|
1508
|
+
foundResult = true;
|
|
1509
|
+
if (typeof resultPromiseResolve === 'function') resultPromiseResolve();
|
|
1510
|
+
}
|
|
1511
|
+
} catch {}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (typeof cb === 'function') cb(err);
|
|
1515
|
+
};
|
|
1516
|
+
return originalWrite(chunk, enc, combinedCallback);
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
return {
|
|
1520
|
+
waitForResult() {
|
|
1521
|
+
if (foundResult) return Promise.resolve();
|
|
1522
|
+
return new Promise((resolve, reject) => {
|
|
1523
|
+
resultPromiseResolve = resolve;
|
|
1524
|
+
});
|
|
1525
|
+
},
|
|
1526
|
+
restore() {
|
|
1527
|
+
if (restored) return;
|
|
1528
|
+
restored = true;
|
|
1529
|
+
if (hadOwnWrite) {
|
|
1530
|
+
Object.defineProperty(process.stdout, 'write', originalWriteDescriptor);
|
|
1531
|
+
} else {
|
|
1532
|
+
delete process.stdout.write;
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function forceTimeoutExit(exitCode) {
|
|
1539
|
+
try { if (streamJsonWatcher) streamJsonWatcher.restore(); } catch {}
|
|
1540
|
+
if (extractedFile) {
|
|
1541
|
+
try { fs.rmSync(extractedFile, { force: true }); } catch {}
|
|
1542
|
+
}
|
|
1543
|
+
originalExit(exitCode);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function waitForPrintFlushIfNeeded() {
|
|
1547
|
+
if (process.env.CLAUDE_TERMUX_PRINT_MODE !== '1') return;
|
|
1548
|
+
if (isStreamJsonPrintMode(argv) && streamJsonWatcher) {
|
|
1549
|
+
let timedOut = false;
|
|
1550
|
+
const rawResultTimeoutMs = Number(process.env.CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS);
|
|
1551
|
+
const resultTimeoutMs = (Number.isFinite(rawResultTimeoutMs) && rawResultTimeoutMs > 0) ? rawResultTimeoutMs : 300000;
|
|
1552
|
+
let timeoutHandle;
|
|
1553
|
+
const timeoutPromise = new Promise(resolve => {
|
|
1554
|
+
timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, resultTimeoutMs);
|
|
1555
|
+
});
|
|
1556
|
+
try {
|
|
1557
|
+
await Promise.race([streamJsonWatcher.waitForResult(), timeoutPromise]);
|
|
1558
|
+
} finally {
|
|
1559
|
+
clearTimeout(timeoutHandle);
|
|
1560
|
+
}
|
|
1561
|
+
if (timedOut) {
|
|
1562
|
+
forceTimeoutExit(1);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const printWaitMs = Number(process.env.CLAUDE_TERMUX_PRINT_WAIT_MS || 5000);
|
|
1568
|
+
if (Number.isFinite(printWaitMs) && printWaitMs > 0) {
|
|
1569
|
+
await new Promise(resolve => setTimeout(resolve, printWaitMs));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1333
1573
|
try {
|
|
1334
1574
|
process.once('uncaughtException', onAsyncError);
|
|
1335
1575
|
process.once('unhandledRejection', onAsyncError);
|
|
@@ -1387,17 +1627,31 @@ async function main() {
|
|
|
1387
1627
|
globalThis.__claudeBun = globalThis.__claudeBunShim;
|
|
1388
1628
|
globalThis.Bun = globalThis.__claudeBunShim;
|
|
1389
1629
|
process.argv = ['node', extractedFile, ...argv];
|
|
1630
|
+
let lastExitAttemptCode = 0;
|
|
1390
1631
|
process.exit = code => {
|
|
1391
|
-
|
|
1632
|
+
lastExitAttemptCode = code ?? 0;
|
|
1633
|
+
throw new RequestedExit(lastExitAttemptCode);
|
|
1392
1634
|
};
|
|
1635
|
+
process.kill = (pid, signal) => {
|
|
1636
|
+
if (pid === process.pid && (signal === 'SIGKILL' || signal === 9)) {
|
|
1637
|
+
throw new RequestedExit(lastExitAttemptCode);
|
|
1638
|
+
}
|
|
1639
|
+
return originalKill.call(process, pid, signal);
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
if (isStreamJsonPrintMode(argv)) {
|
|
1643
|
+
streamJsonWatcher = installStreamJsonTerminalWatcher();
|
|
1644
|
+
}
|
|
1393
1645
|
|
|
1394
1646
|
const moduleLike = { exports: {} };
|
|
1395
1647
|
const maybePromise = fn(moduleLike.exports, fakeRequire, moduleLike, extractedFile, workdir);
|
|
1396
1648
|
if (maybePromise && typeof maybePromise.then === 'function') await maybePromise;
|
|
1649
|
+
await waitForPrintFlushIfNeeded();
|
|
1397
1650
|
if (asyncErrors.length > 0) throw asyncErrors[0];
|
|
1398
1651
|
} catch (error) {
|
|
1399
1652
|
if (error instanceof RequestedExit) {
|
|
1400
1653
|
process.exitCode = error.code;
|
|
1654
|
+
await waitForPrintFlushIfNeeded();
|
|
1401
1655
|
return;
|
|
1402
1656
|
}
|
|
1403
1657
|
throw error;
|
|
@@ -1406,6 +1660,10 @@ async function main() {
|
|
|
1406
1660
|
process.removeListener('unhandledRejection', onAsyncError);
|
|
1407
1661
|
process.argv = originalArgv;
|
|
1408
1662
|
process.exit = originalExit;
|
|
1663
|
+
process.kill = originalKill;
|
|
1664
|
+
if (streamJsonWatcher) {
|
|
1665
|
+
try { streamJsonWatcher.restore(); } catch {}
|
|
1666
|
+
}
|
|
1409
1667
|
process.once('exit', () => {
|
|
1410
1668
|
if (extractedFile) {
|
|
1411
1669
|
try {
|
|
@@ -191,12 +191,32 @@ test('print path does not defer cleanup to exit', () => {
|
|
|
191
191
|
test('bootstrap path defers cleanup to exit', () => {
|
|
192
192
|
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
193
193
|
|
|
194
|
-
assert.equal(bootstrapBlock.includes('CLAUDE_TERMUX_PRINT_WAIT_MS'), false);
|
|
195
|
-
assert.equal(bootstrapBlock.includes('setTimeout(resolve, printWaitMs)'), false);
|
|
196
194
|
assert.equal(bootstrapBlock.includes('process.once(\'exit\''), true);
|
|
197
195
|
assert.equal(bootstrapBlock.includes('process.removeListener(\'uncaughtException\''), true);
|
|
198
196
|
});
|
|
199
197
|
|
|
198
|
+
test('bootstrap branch exports CLAUDE_TERMUX_PRINT_MODE before invoking node', () => {
|
|
199
|
+
const bootstrapShellRegion = extractFunction(
|
|
200
|
+
script,
|
|
201
|
+
'export CLAUDE_TERMUX_TUI="${_tui}"',
|
|
202
|
+
'cat <<\'NODE\' > "$_bootstrap"',
|
|
203
|
+
);
|
|
204
|
+
assert.equal(bootstrapShellRegion.includes('export CLAUDE_TERMUX_PRINT_MODE="${_pf}"'), true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('helper and bootstrap wait for print flush, including the RequestedExit path', () => {
|
|
208
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
209
|
+
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
210
|
+
|
|
211
|
+
assert.equal(helperBlock.includes('async function waitForPrintFlush()'), true);
|
|
212
|
+
assert.equal(helperBlock.includes('await waitForPrintFlush();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
|
|
213
|
+
assert.equal(helperBlock.includes('process.exitCode = error.code;\n await waitForPrintFlush();\n return;'), true);
|
|
214
|
+
|
|
215
|
+
assert.equal(bootstrapBlock.includes('async function waitForPrintFlushIfNeeded()'), true);
|
|
216
|
+
assert.equal(bootstrapBlock.includes('await waitForPrintFlushIfNeeded();\n if (asyncErrors.length > 0) throw asyncErrors[0];'), true);
|
|
217
|
+
assert.equal(bootstrapBlock.includes('process.exitCode = error.code;\n await waitForPrintFlushIfNeeded();\n return;'), true);
|
|
218
|
+
});
|
|
219
|
+
|
|
200
220
|
test('entry extraction uses a process-unique filename', () => {
|
|
201
221
|
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
202
222
|
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
@@ -544,3 +564,777 @@ test('tarball contents match the workspace runner and test file', { skip: !fs.ex
|
|
|
544
564
|
test('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is never forced (regression guard)', () => {
|
|
545
565
|
assert.equal(script.includes('CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC'), false);
|
|
546
566
|
});
|
|
567
|
+
|
|
568
|
+
function buildScenarioFixtureSource() {
|
|
569
|
+
const typeofBun = Array.from({ length: 7 }, () => 'typeof Bun').join('; ');
|
|
570
|
+
const bunProps = Array.from({ length: 42 }, (_, i) => `Bun.p${i}`).join('; ');
|
|
571
|
+
return `function(exports, require, module, __filename, __dirname) {
|
|
572
|
+
${typeofBun}; typeof globalThis.Bun; globalThis.Bun; ${bunProps};
|
|
573
|
+
npmInstallDeprecated:!0; npmInstallDeprecated:!0;
|
|
574
|
+
const scenario = process.env.TEST_SCENARIO;
|
|
575
|
+
if (scenario === 'sync-exit') { process.stdout.write('ok'); process.exit(0); return; }
|
|
576
|
+
if (scenario === 'async-exit') {
|
|
577
|
+
setTimeout(() => { process.stdout.write('ok'); }, 100).unref();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (scenario === 'self-sigkill-fallback-string') {
|
|
581
|
+
try {
|
|
582
|
+
process.exit(17);
|
|
583
|
+
} catch (e) {
|
|
584
|
+
process.kill(process.pid, 'SIGKILL');
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (scenario === 'self-sigkill-fallback-numeric') {
|
|
589
|
+
try {
|
|
590
|
+
process.exit(17);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
process.kill(process.pid, 9);
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (scenario === 'self-sigkill-fallback-no-code') {
|
|
597
|
+
try {
|
|
598
|
+
process.exit();
|
|
599
|
+
} catch (e) {
|
|
600
|
+
process.kill(process.pid, 'SIGKILL');
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (scenario === 'other-process-kill') {
|
|
605
|
+
const cp = require('child_process');
|
|
606
|
+
const child = cp.spawn('node', ['-e', 'setTimeout(() => {}, 5000)']);
|
|
607
|
+
const childPid = child.pid;
|
|
608
|
+
process.kill(childPid, 0);
|
|
609
|
+
child.kill();
|
|
610
|
+
process.stdout.write('ok');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (scenario === 'stream-json-result') {
|
|
614
|
+
process.stdout.write('{"type":"init"}\\n');
|
|
615
|
+
const msg = '{"type":"result","data":"test"}\\n';
|
|
616
|
+
process.stdout.write(msg, undefined, () => {});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (scenario === 'stream-json-multibyte-split') {
|
|
620
|
+
// Split a UTF-8 multi-byte character across write calls
|
|
621
|
+
// 'あ' is 3 bytes in UTF-8: e3 81 82
|
|
622
|
+
const buf = Buffer.from('あ', 'utf8');
|
|
623
|
+
// Split the 3-byte character: first byte in one write, remaining in another
|
|
624
|
+
const jsonLine = '{"type":"result"}\\n';
|
|
625
|
+
const part1 = Buffer.concat([Buffer.from(jsonLine), buf.slice(0, 1)]);
|
|
626
|
+
const part2 = Buffer.concat([buf.slice(1)]);
|
|
627
|
+
process.stdout.write(part1);
|
|
628
|
+
process.stdout.write(part2, undefined, () => {});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (scenario === 'stream-json-timeout') {
|
|
632
|
+
process.stdout.write('{"type":"init"}\\n');
|
|
633
|
+
setTimeout(() => {}, 5000);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (scenario === 'stream-json-requested-exit-then-result') {
|
|
637
|
+
process.stdout.write('{"type":"result"}\\n');
|
|
638
|
+
process.exit(0);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
process.stdout.write('ok');
|
|
642
|
+
}`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function runScenario({ printMode, stdinInherit, scenario, extraArgs }) {
|
|
646
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stdin-test-'));
|
|
647
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
648
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
649
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
650
|
+
const entryJsOffset = 0;
|
|
651
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
652
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
653
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
654
|
+
|
|
655
|
+
const env = {
|
|
656
|
+
...process.env,
|
|
657
|
+
SOURCE_BIN: sourceBin,
|
|
658
|
+
WORKDIR: workdir,
|
|
659
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
660
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
661
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
662
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
663
|
+
MAGI_ENV: '1',
|
|
664
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
665
|
+
TMPDIR: tmpBase,
|
|
666
|
+
TEST_SCENARIO: scenario,
|
|
667
|
+
};
|
|
668
|
+
if (stdinInherit) env.CLAUDE_TERMUX_STDIN = 'inherit';
|
|
669
|
+
else delete env.CLAUDE_TERMUX_STDIN;
|
|
670
|
+
|
|
671
|
+
const args = printMode ? ['-p', 'x', ...(extraArgs || [])] : [];
|
|
672
|
+
const start = Date.now();
|
|
673
|
+
const result = child_process.spawnSync('sh', [scriptPath, ...args], {
|
|
674
|
+
env,
|
|
675
|
+
input: 'test input\n',
|
|
676
|
+
encoding: 'utf8',
|
|
677
|
+
timeout: 10000,
|
|
678
|
+
});
|
|
679
|
+
const elapsedMs = Date.now() - start;
|
|
680
|
+
return { ...result, elapsedMs, tmpBase };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
test('helper and bootstrap intercept process.kill(self, SIGKILL) statically', () => {
|
|
684
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
685
|
+
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
686
|
+
|
|
687
|
+
// Check helper branch
|
|
688
|
+
const helperOriginalKillIdx = helperBlock.indexOf('const originalKill = process.kill;');
|
|
689
|
+
assert.ok(helperOriginalKillIdx > -1, 'helper: missing originalKill declaration');
|
|
690
|
+
|
|
691
|
+
const helperProcessKillIdx = helperBlock.indexOf('process.kill = (pid, signal) => {', helperOriginalKillIdx);
|
|
692
|
+
assert.ok(helperProcessKillIdx > helperOriginalKillIdx, 'helper: process.kill override must come after originalKill declaration');
|
|
693
|
+
|
|
694
|
+
const helperRestoreIdx = helperBlock.indexOf('process.kill = originalKill;', helperProcessKillIdx);
|
|
695
|
+
assert.ok(helperRestoreIdx > helperProcessKillIdx, 'helper: process.kill restoration must come after override');
|
|
696
|
+
|
|
697
|
+
// Verify SIGKILL check within the override
|
|
698
|
+
const helperKillOverrideEnd = helperBlock.indexOf('};', helperProcessKillIdx);
|
|
699
|
+
const helperKillOverride = helperBlock.slice(helperProcessKillIdx, helperKillOverrideEnd);
|
|
700
|
+
assert.ok(helperKillOverride.includes('signal === \'SIGKILL\''), 'helper: process.kill must check for SIGKILL string');
|
|
701
|
+
assert.ok(helperKillOverride.includes('signal === 9'), 'helper: process.kill must check for SIGKILL numeric value');
|
|
702
|
+
assert.ok(helperKillOverride.includes('throw new RequestedExit'), 'helper: process.kill must throw RequestedExit for self SIGKILL');
|
|
703
|
+
|
|
704
|
+
// Check bootstrap branch
|
|
705
|
+
const bootstrapOriginalKillIdx = bootstrapBlock.indexOf('const originalKill = process.kill;');
|
|
706
|
+
assert.ok(bootstrapOriginalKillIdx > -1, 'bootstrap: missing originalKill declaration');
|
|
707
|
+
|
|
708
|
+
const bootstrapProcessKillIdx = bootstrapBlock.indexOf('process.kill = (pid, signal) => {', bootstrapOriginalKillIdx);
|
|
709
|
+
assert.ok(bootstrapProcessKillIdx > bootstrapOriginalKillIdx, 'bootstrap: process.kill override must come after originalKill declaration');
|
|
710
|
+
|
|
711
|
+
const bootstrapRestoreIdx = bootstrapBlock.indexOf('process.kill = originalKill;', bootstrapProcessKillIdx);
|
|
712
|
+
assert.ok(bootstrapRestoreIdx > bootstrapProcessKillIdx, 'bootstrap: process.kill restoration must come after override');
|
|
713
|
+
|
|
714
|
+
// Verify SIGKILL check within the override
|
|
715
|
+
const bootstrapKillOverrideEnd = bootstrapBlock.indexOf('};', bootstrapProcessKillIdx);
|
|
716
|
+
const bootstrapKillOverride = bootstrapBlock.slice(bootstrapProcessKillIdx, bootstrapKillOverrideEnd);
|
|
717
|
+
assert.ok(bootstrapKillOverride.includes('signal === \'SIGKILL\''), 'bootstrap: process.kill must check for SIGKILL string');
|
|
718
|
+
assert.ok(bootstrapKillOverride.includes('signal === 9'), 'bootstrap: process.kill must check for SIGKILL numeric value');
|
|
719
|
+
assert.ok(bootstrapKillOverride.includes('throw new RequestedExit'), 'bootstrap: process.kill must throw RequestedExit for self SIGKILL');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test('helper branch (CLI -p, no stdin inherit): normal/sync-exit/async-exit all produce output', () => {
|
|
723
|
+
for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
|
|
724
|
+
const r = runScenario({ printMode: true, stdinInherit: false, scenario });
|
|
725
|
+
try {
|
|
726
|
+
assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
|
|
727
|
+
assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
|
|
728
|
+
} finally {
|
|
729
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit): normal/sync-exit/async-exit all produce output', () => {
|
|
735
|
+
for (const scenario of ['normal', 'sync-exit', 'async-exit']) {
|
|
736
|
+
const r = runScenario({ printMode: true, stdinInherit: true, scenario });
|
|
737
|
+
try {
|
|
738
|
+
assert.ok((r.stdout || '').includes('ok'), `scenario=${scenario} stdout=${r.stdout} stderr=${r.stderr}`);
|
|
739
|
+
assert.equal(r.status, 0, `scenario=${scenario} status=${r.status} stderr=${r.stderr}`);
|
|
740
|
+
if (scenario === 'async-exit') {
|
|
741
|
+
assert.ok(r.elapsedMs >= 250, `expected wait >= 250ms, got ${r.elapsedMs}ms`);
|
|
742
|
+
}
|
|
743
|
+
} finally {
|
|
744
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('helper branch intercepts self-directed SIGKILL (string signal) and exits with proper code', () => {
|
|
750
|
+
const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-string' });
|
|
751
|
+
try {
|
|
752
|
+
assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
|
|
753
|
+
assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
|
|
754
|
+
} finally {
|
|
755
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('helper branch intercepts self-directed SIGKILL (numeric signal 9) and exits with proper code', () => {
|
|
760
|
+
const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-numeric' });
|
|
761
|
+
try {
|
|
762
|
+
assert.equal(r.status, 17, `expected status 17, got ${r.status}; stderr=${r.stderr}`);
|
|
763
|
+
assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
|
|
764
|
+
} finally {
|
|
765
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test('helper branch intercepts self-directed SIGKILL with process.exit() (no code) and defaults to 0', () => {
|
|
770
|
+
const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'self-sigkill-fallback-no-code' });
|
|
771
|
+
try {
|
|
772
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
773
|
+
assert.ok(!r.signal, `expected clean exit (no signal), but got signal: ${r.signal}`);
|
|
774
|
+
} finally {
|
|
775
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('helper branch allows process.kill to other processes (signal 0, passthrough)', () => {
|
|
780
|
+
const r = runScenario({ printMode: true, stdinInherit: false, scenario: 'other-process-kill' });
|
|
781
|
+
try {
|
|
782
|
+
assert.ok((r.stdout || '').includes('ok'), `expected ok output, got stdout=${r.stdout} stderr=${r.stderr}`);
|
|
783
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
784
|
+
} finally {
|
|
785
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test('isStreamJsonPrintMode detects print flag and stream-json format (case 1: -p + format + value)', () => {
|
|
790
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
791
|
+
const fnSource = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
|
|
792
|
+
const context = vm.createContext({ module: { exports: {} } });
|
|
793
|
+
vm.runInContext(`${fnSource}\nmodule.exports = isStreamJsonPrintMode;`, context);
|
|
794
|
+
const isStreamJsonPrintMode = context.module.exports;
|
|
795
|
+
|
|
796
|
+
assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'stream-json']), true, 'case 1');
|
|
797
|
+
assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format=stream-json']), true, 'case 2');
|
|
798
|
+
assert.equal(isStreamJsonPrintMode(['--print', '--output-format=stream-json']), true, 'case 3');
|
|
799
|
+
assert.equal(isStreamJsonPrintMode(['-p', 'hello', '--output-format', 'json']), false, 'case 4');
|
|
800
|
+
assert.equal(isStreamJsonPrintMode(['-p']), false, 'case 5');
|
|
801
|
+
assert.equal(isStreamJsonPrintMode(['--output-format=stream-json']), false, 'case 6');
|
|
802
|
+
assert.equal(isStreamJsonPrintMode(['-p', '--', '--output-format=stream-json']), false, 'case 7');
|
|
803
|
+
assert.equal(isStreamJsonPrintMode(['-p', '--output-format=stream-json', '--', 'extra']), true, 'case 8');
|
|
804
|
+
assert.equal(isStreamJsonPrintMode(['-p', '--output-format', '--', 'stream-json']), false, 'case 9');
|
|
805
|
+
assert.equal(isStreamJsonPrintMode(['-p', '--output-format']), false, 'case 10');
|
|
806
|
+
assert.equal(isStreamJsonPrintMode([]), false, 'case 11');
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test('isStreamJsonPrintMode is identical in helper and bootstrap', () => {
|
|
810
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
811
|
+
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
812
|
+
|
|
813
|
+
const helperFn = extractFunction(helperBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
|
|
814
|
+
const bootstrapFn = extractFunction(bootstrapBlock, 'function isStreamJsonPrintMode(argv) {', '\n\nclass RequestedExit');
|
|
815
|
+
|
|
816
|
+
assert.equal(helperFn, bootstrapFn, 'isStreamJsonPrintMode must be identical in both heredocs');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on NaN', () => {
|
|
820
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
|
|
821
|
+
try {
|
|
822
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
823
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
824
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
825
|
+
const entryJsOffset = 0;
|
|
826
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
827
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
828
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
829
|
+
|
|
830
|
+
const env = {
|
|
831
|
+
...process.env,
|
|
832
|
+
SOURCE_BIN: sourceBin,
|
|
833
|
+
WORKDIR: workdir,
|
|
834
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
835
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
836
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
837
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
838
|
+
MAGI_ENV: '1',
|
|
839
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
840
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: 'invalid',
|
|
841
|
+
TMPDIR: tmpBase,
|
|
842
|
+
TEST_SCENARIO: 'stream-json-result',
|
|
843
|
+
};
|
|
844
|
+
delete env.CLAUDE_TERMUX_STDIN;
|
|
845
|
+
|
|
846
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
847
|
+
env,
|
|
848
|
+
input: 'test input\n',
|
|
849
|
+
encoding: 'utf8',
|
|
850
|
+
timeout: 10000,
|
|
851
|
+
});
|
|
852
|
+
assert.equal(result.status, 0, `expected successful exit with invalid timeout fallback, got status=${result.status} stderr=${result.stderr}`);
|
|
853
|
+
} finally {
|
|
854
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test('CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS fallback to 300000 on zero', () => {
|
|
859
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-timeout-test-'));
|
|
860
|
+
try {
|
|
861
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
862
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
863
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
864
|
+
const entryJsOffset = 0;
|
|
865
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
866
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
867
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
868
|
+
|
|
869
|
+
const env = {
|
|
870
|
+
...process.env,
|
|
871
|
+
SOURCE_BIN: sourceBin,
|
|
872
|
+
WORKDIR: workdir,
|
|
873
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
874
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
875
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
876
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
877
|
+
MAGI_ENV: '1',
|
|
878
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
879
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '0',
|
|
880
|
+
TMPDIR: tmpBase,
|
|
881
|
+
TEST_SCENARIO: 'stream-json-result',
|
|
882
|
+
};
|
|
883
|
+
delete env.CLAUDE_TERMUX_STDIN;
|
|
884
|
+
|
|
885
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
886
|
+
env,
|
|
887
|
+
input: 'test input\n',
|
|
888
|
+
encoding: 'utf8',
|
|
889
|
+
timeout: 10000,
|
|
890
|
+
});
|
|
891
|
+
assert.equal(result.status, 0, `expected successful exit with zero timeout fallback, got status=${result.status} stderr=${result.stderr}`);
|
|
892
|
+
} finally {
|
|
893
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
test('helper branch (CLI -p, no stdin inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
|
|
898
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-'));
|
|
899
|
+
try {
|
|
900
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
901
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
902
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
903
|
+
const entryJsOffset = 0;
|
|
904
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
905
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
906
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
907
|
+
|
|
908
|
+
const env = {
|
|
909
|
+
...process.env,
|
|
910
|
+
SOURCE_BIN: sourceBin,
|
|
911
|
+
WORKDIR: workdir,
|
|
912
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
913
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
914
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
915
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
916
|
+
MAGI_ENV: '1',
|
|
917
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
918
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
|
|
919
|
+
TMPDIR: tmpBase,
|
|
920
|
+
TEST_SCENARIO: 'stream-json-result',
|
|
921
|
+
};
|
|
922
|
+
delete env.CLAUDE_TERMUX_STDIN;
|
|
923
|
+
|
|
924
|
+
const start = Date.now();
|
|
925
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
926
|
+
env,
|
|
927
|
+
input: 'test input\n',
|
|
928
|
+
encoding: 'utf8',
|
|
929
|
+
timeout: 20000,
|
|
930
|
+
});
|
|
931
|
+
const elapsedMs = Date.now() - start;
|
|
932
|
+
|
|
933
|
+
assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
|
|
934
|
+
assert.ok(
|
|
935
|
+
elapsedMs < 3000,
|
|
936
|
+
`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.`
|
|
937
|
+
);
|
|
938
|
+
} finally {
|
|
939
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test('bootstrap branch (-p + CLAUDE_TERMUX_STDIN=inherit) stream-json: result detected -> exits immediately without waiting for timeout', () => {
|
|
944
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-json-clear-timeout-bootstrap-'));
|
|
945
|
+
try {
|
|
946
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
947
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
948
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
949
|
+
const entryJsOffset = 0;
|
|
950
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
951
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
952
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
953
|
+
|
|
954
|
+
const env = {
|
|
955
|
+
...process.env,
|
|
956
|
+
SOURCE_BIN: sourceBin,
|
|
957
|
+
WORKDIR: workdir,
|
|
958
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
959
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
960
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
961
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
962
|
+
MAGI_ENV: '1',
|
|
963
|
+
CLAUDE_TERMUX_STDIN: 'inherit',
|
|
964
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
965
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '10000',
|
|
966
|
+
TMPDIR: tmpBase,
|
|
967
|
+
TEST_SCENARIO: 'stream-json-result',
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
const start = Date.now();
|
|
971
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
972
|
+
env,
|
|
973
|
+
input: 'test input\n',
|
|
974
|
+
encoding: 'utf8',
|
|
975
|
+
timeout: 20000,
|
|
976
|
+
});
|
|
977
|
+
const elapsedMs = Date.now() - start;
|
|
978
|
+
|
|
979
|
+
assert.equal(result.status, 0, `expected successful exit, got status=${result.status} stderr=${result.stderr}`);
|
|
980
|
+
assert.ok(
|
|
981
|
+
elapsedMs < 3000,
|
|
982
|
+
`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.`
|
|
983
|
+
);
|
|
984
|
+
} finally {
|
|
985
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test('helper and bootstrap installStreamJsonTerminalWatcher helpers stay identical', () => {
|
|
990
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
991
|
+
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
992
|
+
|
|
993
|
+
const helperWatcher = extractFunction(
|
|
994
|
+
helperBlock,
|
|
995
|
+
'function installStreamJsonTerminalWatcher() {',
|
|
996
|
+
'\n function forceTimeoutExit',
|
|
997
|
+
);
|
|
998
|
+
const bootstrapWatcher = extractFunction(
|
|
999
|
+
bootstrapBlock,
|
|
1000
|
+
'function installStreamJsonTerminalWatcher() {',
|
|
1001
|
+
'\n function forceTimeoutExit',
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
assert.equal(helperWatcher, bootstrapWatcher, 'installStreamJsonTerminalWatcher must be identical in both heredocs');
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test('helper and bootstrap forceTimeoutExit helpers stay identical', () => {
|
|
1008
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
1009
|
+
const bootstrapBlock = extractBlock('cat <<\'NODE\' > "$_bootstrap"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
1010
|
+
|
|
1011
|
+
const helperForceExit = extractFunction(
|
|
1012
|
+
helperBlock,
|
|
1013
|
+
'function forceTimeoutExit(exitCode) {',
|
|
1014
|
+
'\n async function waitForPrintFlush',
|
|
1015
|
+
);
|
|
1016
|
+
const bootstrapForceExit = extractFunction(
|
|
1017
|
+
bootstrapBlock,
|
|
1018
|
+
'function forceTimeoutExit(exitCode) {',
|
|
1019
|
+
'\n async function waitForPrintFlushIfNeeded',
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
assert.equal(helperForceExit, bootstrapForceExit, 'forceTimeoutExit must be identical in both heredocs');
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test('installStreamJsonTerminalWatcher restores process.stdout.write own property state', () => {
|
|
1026
|
+
// Test with the real process.stdout to verify own property handling
|
|
1027
|
+
const hadOwnPropertyBefore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1028
|
+
const descriptorBefore = hadOwnPropertyBefore ? Object.getOwnPropertyDescriptor(process.stdout, 'write') : undefined;
|
|
1029
|
+
|
|
1030
|
+
// Get the watcher function from helper
|
|
1031
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
1032
|
+
const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
|
|
1033
|
+
|
|
1034
|
+
const context = vm.createContext({
|
|
1035
|
+
module: { exports: {} },
|
|
1036
|
+
process,
|
|
1037
|
+
Object,
|
|
1038
|
+
Buffer,
|
|
1039
|
+
require: (id) => {
|
|
1040
|
+
if (id === 'string_decoder') return require('string_decoder');
|
|
1041
|
+
throw new Error('require not available');
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
vm.runInContext(`
|
|
1046
|
+
${watcherSource}
|
|
1047
|
+
module.exports = installStreamJsonTerminalWatcher;
|
|
1048
|
+
`, context);
|
|
1049
|
+
|
|
1050
|
+
const installStreamJsonTerminalWatcher = context.module.exports;
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
// Test case 1: Normal case where write is not an own property
|
|
1054
|
+
{
|
|
1055
|
+
const watcher = installStreamJsonTerminalWatcher();
|
|
1056
|
+
const hadOwnPropertyAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1057
|
+
assert.equal(hadOwnPropertyAfterInstall, true, 'after install: process.stdout.write should be own property');
|
|
1058
|
+
|
|
1059
|
+
// Restore watcher
|
|
1060
|
+
watcher.restore();
|
|
1061
|
+
const hadOwnPropertyAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1062
|
+
assert.equal(hadOwnPropertyAfterRestore, hadOwnPropertyBefore, 'after restore: own property state should match initial');
|
|
1063
|
+
|
|
1064
|
+
// Verify restore is idempotent
|
|
1065
|
+
watcher.restore();
|
|
1066
|
+
const hadOwnPropertyAfterSecondRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1067
|
+
assert.equal(hadOwnPropertyAfterSecondRestore, hadOwnPropertyBefore, 'second restore should also maintain initial state');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Test case 2: When write is an own property before installation
|
|
1071
|
+
{
|
|
1072
|
+
const testDescriptor = {
|
|
1073
|
+
value: function testWrite() { return true; },
|
|
1074
|
+
writable: true,
|
|
1075
|
+
configurable: true,
|
|
1076
|
+
enumerable: false,
|
|
1077
|
+
};
|
|
1078
|
+
Object.defineProperty(process.stdout, 'write', testDescriptor);
|
|
1079
|
+
|
|
1080
|
+
const watcher = installStreamJsonTerminalWatcher();
|
|
1081
|
+
const hadOwnAfterInstall = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1082
|
+
assert.equal(hadOwnAfterInstall, true, 'test case 2: after install should have own property');
|
|
1083
|
+
|
|
1084
|
+
watcher.restore();
|
|
1085
|
+
const hadOwnAfterRestore = Object.prototype.hasOwnProperty.call(process.stdout, 'write');
|
|
1086
|
+
assert.equal(hadOwnAfterRestore, true, 'test case 2: after restore should still have own property');
|
|
1087
|
+
|
|
1088
|
+
const restoredDescriptor = Object.getOwnPropertyDescriptor(process.stdout, 'write');
|
|
1089
|
+
assert.equal(typeof restoredDescriptor.value, 'function', 'test case 2: restored value should be a function');
|
|
1090
|
+
assert.equal(restoredDescriptor.configurable, true, 'test case 2: restored configurable should match');
|
|
1091
|
+
}
|
|
1092
|
+
} finally {
|
|
1093
|
+
// Ensure stdout.write is fully restored to original state
|
|
1094
|
+
if (hadOwnPropertyBefore && descriptorBefore) {
|
|
1095
|
+
Object.defineProperty(process.stdout, 'write', descriptorBefore);
|
|
1096
|
+
} else if (Object.prototype.hasOwnProperty.call(process.stdout, 'write')) {
|
|
1097
|
+
delete process.stdout.write;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test('helper branch stream-json result detection (single write)', () => {
|
|
1103
|
+
const r = runScenario({
|
|
1104
|
+
printMode: true,
|
|
1105
|
+
stdinInherit: false,
|
|
1106
|
+
scenario: 'stream-json-result',
|
|
1107
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1108
|
+
});
|
|
1109
|
+
try {
|
|
1110
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1111
|
+
// Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
|
|
1112
|
+
assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
|
|
1113
|
+
} finally {
|
|
1114
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test('bootstrap branch stream-json result detection (single write)', () => {
|
|
1119
|
+
const r = runScenario({
|
|
1120
|
+
printMode: true,
|
|
1121
|
+
stdinInherit: true,
|
|
1122
|
+
scenario: 'stream-json-result',
|
|
1123
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1124
|
+
});
|
|
1125
|
+
try {
|
|
1126
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1127
|
+
// Result detection should be significantly faster than traditional PRINT_WAIT_MS (300ms in tests)
|
|
1128
|
+
assert.ok(r.elapsedMs < 1500, `expected completion < 1500ms (much faster than 300ms PRINT_WAIT_MS), got ${r.elapsedMs}ms`);
|
|
1129
|
+
} finally {
|
|
1130
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test('helper branch stream-json multibyte character split handling', () => {
|
|
1135
|
+
const r = runScenario({
|
|
1136
|
+
printMode: true,
|
|
1137
|
+
stdinInherit: false,
|
|
1138
|
+
scenario: 'stream-json-multibyte-split',
|
|
1139
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1140
|
+
});
|
|
1141
|
+
try {
|
|
1142
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1143
|
+
assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
|
|
1144
|
+
} finally {
|
|
1145
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
test('bootstrap branch stream-json multibyte character split handling', () => {
|
|
1150
|
+
const r = runScenario({
|
|
1151
|
+
printMode: true,
|
|
1152
|
+
stdinInherit: true,
|
|
1153
|
+
scenario: 'stream-json-multibyte-split',
|
|
1154
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1155
|
+
});
|
|
1156
|
+
try {
|
|
1157
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1158
|
+
assert.ok(r.elapsedMs < 6000, `expected completion < 6000ms, got ${r.elapsedMs}ms`);
|
|
1159
|
+
} finally {
|
|
1160
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
test('helper branch stream-json timeout triggers exit with status 1', () => {
|
|
1165
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
|
|
1166
|
+
try {
|
|
1167
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
1168
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
1169
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
1170
|
+
const entryJsOffset = 0;
|
|
1171
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
1172
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
1173
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
1174
|
+
|
|
1175
|
+
const env = {
|
|
1176
|
+
...process.env,
|
|
1177
|
+
SOURCE_BIN: sourceBin,
|
|
1178
|
+
WORKDIR: workdir,
|
|
1179
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
1180
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
1181
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
1182
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
1183
|
+
MAGI_ENV: '1',
|
|
1184
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
1185
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
|
|
1186
|
+
TMPDIR: tmpBase,
|
|
1187
|
+
TEST_SCENARIO: 'stream-json-timeout',
|
|
1188
|
+
};
|
|
1189
|
+
delete env.CLAUDE_TERMUX_STDIN;
|
|
1190
|
+
|
|
1191
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
1192
|
+
env,
|
|
1193
|
+
input: 'test input\n',
|
|
1194
|
+
encoding: 'utf8',
|
|
1195
|
+
timeout: 5000,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
|
|
1199
|
+
const entries = fs.readdirSync(workdir, { withFileTypes: true });
|
|
1200
|
+
const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
|
|
1201
|
+
assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
|
|
1202
|
+
} finally {
|
|
1203
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
test('bootstrap branch stream-json timeout triggers exit with status 1', () => {
|
|
1208
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-stream-timeout-'));
|
|
1209
|
+
try {
|
|
1210
|
+
const sourceBin = path.join(tmpBase, 'fake-source.js');
|
|
1211
|
+
const fixtureSource = buildScenarioFixtureSource();
|
|
1212
|
+
fs.writeFileSync(sourceBin, fixtureSource, 'utf8');
|
|
1213
|
+
const entryJsOffset = 0;
|
|
1214
|
+
const entryEndOffset = Buffer.byteLength(fixtureSource, 'utf8');
|
|
1215
|
+
const workdir = path.join(tmpBase, 'workdir');
|
|
1216
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
1217
|
+
|
|
1218
|
+
const env = {
|
|
1219
|
+
...process.env,
|
|
1220
|
+
SOURCE_BIN: sourceBin,
|
|
1221
|
+
WORKDIR: workdir,
|
|
1222
|
+
ENTRY_JS_OFFSET: String(entryJsOffset),
|
|
1223
|
+
ENTRY_END_OFFSET: String(entryEndOffset),
|
|
1224
|
+
CURRENT_CLAUDE_VERSION: '2.1.220',
|
|
1225
|
+
CLAUDE_TERMUX_PACKAGE_DIR: path.join(__dirname, '..'),
|
|
1226
|
+
MAGI_ENV: '1',
|
|
1227
|
+
CLAUDE_TERMUX_PRINT_WAIT_MS: '300',
|
|
1228
|
+
CLAUDE_TERMUX_PRINT_RESULT_TIMEOUT_MS: '300',
|
|
1229
|
+
CLAUDE_TERMUX_STDIN: 'inherit',
|
|
1230
|
+
TMPDIR: tmpBase,
|
|
1231
|
+
TEST_SCENARIO: 'stream-json-timeout',
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
const result = child_process.spawnSync('sh', [scriptPath, '-p', 'x', '--output-format=stream-json'], {
|
|
1235
|
+
env,
|
|
1236
|
+
input: 'test input\n',
|
|
1237
|
+
encoding: 'utf8',
|
|
1238
|
+
timeout: 5000,
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
assert.equal(result.status, 1, `expected status 1 on timeout, got ${result.status}; stderr=${result.stderr}`);
|
|
1242
|
+
const entries = fs.readdirSync(workdir, { withFileTypes: true });
|
|
1243
|
+
const entryFiles = entries.filter(e => e.name.includes('cli.') && e.name.endsWith('.bare-path.js'));
|
|
1244
|
+
assert.equal(entryFiles.length, 0, `expected no extracted entry files after timeout, found ${entryFiles.length}`);
|
|
1245
|
+
} finally {
|
|
1246
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test('helper branch stream-json requested exit after result', () => {
|
|
1251
|
+
const r = runScenario({
|
|
1252
|
+
printMode: true,
|
|
1253
|
+
stdinInherit: false,
|
|
1254
|
+
scenario: 'stream-json-requested-exit-then-result',
|
|
1255
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1256
|
+
});
|
|
1257
|
+
try {
|
|
1258
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1259
|
+
} finally {
|
|
1260
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test('bootstrap branch stream-json requested exit after result', () => {
|
|
1265
|
+
const r = runScenario({
|
|
1266
|
+
printMode: true,
|
|
1267
|
+
stdinInherit: true,
|
|
1268
|
+
scenario: 'stream-json-requested-exit-then-result',
|
|
1269
|
+
extraArgs: ['--output-format=stream-json'],
|
|
1270
|
+
});
|
|
1271
|
+
try {
|
|
1272
|
+
assert.equal(r.status, 0, `expected status 0, got ${r.status}; stderr=${r.stderr}`);
|
|
1273
|
+
} finally {
|
|
1274
|
+
fs.rmSync(r.tmpBase, { recursive: true, force: true });
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test('installStreamJsonTerminalWatcher waits for write callback before completing result', async () => {
|
|
1279
|
+
const helperBlock = extractBlock('cat <<\'NODE\' > "$_helper"', '\n export ENABLE_CLAUDEAI_MCP_SERVERS=');
|
|
1280
|
+
const watcherSource = extractFunction(helperBlock, 'function installStreamJsonTerminalWatcher() {', '\n function forceTimeoutExit');
|
|
1281
|
+
|
|
1282
|
+
// Create a dedicated mock stdout object instead of modifying the real one
|
|
1283
|
+
let callbackFired = false;
|
|
1284
|
+
const mockStdout = Object.create(Object.getPrototypeOf(process.stdout));
|
|
1285
|
+
|
|
1286
|
+
// Copy necessary properties
|
|
1287
|
+
Object.defineProperty(mockStdout, 'write', {
|
|
1288
|
+
value: function(chunk, encoding, callback) {
|
|
1289
|
+
if (typeof encoding === 'function') {
|
|
1290
|
+
callback = encoding;
|
|
1291
|
+
encoding = undefined;
|
|
1292
|
+
}
|
|
1293
|
+
if (callback) {
|
|
1294
|
+
// Defer callback to next microtask
|
|
1295
|
+
setImmediate(() => {
|
|
1296
|
+
callbackFired = true;
|
|
1297
|
+
callback();
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
return true;
|
|
1301
|
+
},
|
|
1302
|
+
writable: true,
|
|
1303
|
+
configurable: true,
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
const context = vm.createContext({
|
|
1307
|
+
module: { exports: {} },
|
|
1308
|
+
process: { stdout: mockStdout },
|
|
1309
|
+
Object,
|
|
1310
|
+
Buffer,
|
|
1311
|
+
require: (id) => {
|
|
1312
|
+
if (id === 'string_decoder') return require('string_decoder');
|
|
1313
|
+
throw new Error('require not available');
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
vm.runInContext(`
|
|
1318
|
+
${watcherSource}
|
|
1319
|
+
module.exports = installStreamJsonTerminalWatcher;
|
|
1320
|
+
`, context);
|
|
1321
|
+
|
|
1322
|
+
const installStreamJsonTerminalWatcher = context.module.exports;
|
|
1323
|
+
const watcher = installStreamJsonTerminalWatcher();
|
|
1324
|
+
|
|
1325
|
+
// Simulate a write with result JSON
|
|
1326
|
+
const resultJson = '{"type":"result","data":"test"}\n';
|
|
1327
|
+
mockStdout.write(resultJson, 'utf8');
|
|
1328
|
+
|
|
1329
|
+
// Get the promise before callback fires
|
|
1330
|
+
const resultPromise = watcher.waitForResult();
|
|
1331
|
+
|
|
1332
|
+
// Give time for callback to fire
|
|
1333
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1334
|
+
|
|
1335
|
+
// Promise should now be resolved
|
|
1336
|
+
await resultPromise;
|
|
1337
|
+
assert.ok(callbackFired, 'callback should have been fired');
|
|
1338
|
+
|
|
1339
|
+
watcher.restore();
|
|
1340
|
+
});
|