@bash0816/claude-code 2.1.248 → 2.1.252
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 +46 -1
- package/bin/claude +2 -2
- package/config/claude-native-audited-versions.json +239 -10
- package/config/claude-termux-release-manifest.json +3 -3
- package/lib/bunfs-esm-loader.mjs +141 -21
- package/lib/bunfs-esm-loader.test.js +875 -0
- package/lib/termux-run-claude-native.sh +13 -2
- package/lib/termux-run-claude-native.test.js +123 -0
- package/package.json +2 -2
|
@@ -486,3 +486,878 @@ test('import.meta.require resolves /$bunfs/root/ specifiers via loader integrati
|
|
|
486
486
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
487
487
|
}
|
|
488
488
|
});
|
|
489
|
+
|
|
490
|
+
// Recovery tests for missing module scenario
|
|
491
|
+
|
|
492
|
+
// T1: resolve() がチャンク欠落を回復する
|
|
493
|
+
test('T1: resolve() recovers chunk deletion by calling reExtract', async () => {
|
|
494
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
495
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t1-${process.pid}-${Date.now()}`);
|
|
496
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
500
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
501
|
+
const guardPath = path.join(tempDir, 'guard.mjs');
|
|
502
|
+
fs.writeFileSync(guardPath, 'export default {};');
|
|
503
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
504
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
505
|
+
|
|
506
|
+
const targetFile = path.join(tempDir, 'target.js');
|
|
507
|
+
fs.writeFileSync(targetFile, 'export const x = 1;');
|
|
508
|
+
|
|
509
|
+
let reExtractCalls = 0;
|
|
510
|
+
loader.initialize({
|
|
511
|
+
processOwnedDir: tempDir,
|
|
512
|
+
sourceBin: sourceBin,
|
|
513
|
+
childProcessGuardPath: guardPath,
|
|
514
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
515
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
516
|
+
reExtract: (sb, od) => {
|
|
517
|
+
reExtractCalls++;
|
|
518
|
+
fs.writeFileSync(targetFile, 'export const x = 1;');
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Delete file
|
|
523
|
+
fs.unlinkSync(targetFile);
|
|
524
|
+
|
|
525
|
+
// resolve() should trigger recovery
|
|
526
|
+
const result = loader.resolve('/$bunfs/root/target.js', {}, () => ({}));
|
|
527
|
+
assert.ok(result.url);
|
|
528
|
+
assert.equal(reExtractCalls, 1);
|
|
529
|
+
assert.ok(fs.existsSync(targetFile));
|
|
530
|
+
} finally {
|
|
531
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// T2: load() が readFileSync ENOENT を回復する
|
|
536
|
+
test('T2: load() recovers readFileSync ENOENT by calling reExtract', async () => {
|
|
537
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
538
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t2-${process.pid}-${Date.now()}`);
|
|
539
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
543
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
544
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
545
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
546
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
547
|
+
|
|
548
|
+
const targetFile = path.join(tempDir, 'target.js');
|
|
549
|
+
const originalSource = 'export const y = 2;';
|
|
550
|
+
fs.writeFileSync(targetFile, originalSource);
|
|
551
|
+
|
|
552
|
+
let reExtractCalls = 0;
|
|
553
|
+
loader.initialize({
|
|
554
|
+
processOwnedDir: tempDir,
|
|
555
|
+
sourceBin: sourceBin,
|
|
556
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
557
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
558
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
559
|
+
reExtract: () => {
|
|
560
|
+
reExtractCalls++;
|
|
561
|
+
fs.writeFileSync(targetFile, originalSource);
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Delete file
|
|
566
|
+
fs.unlinkSync(targetFile);
|
|
567
|
+
|
|
568
|
+
// load() should trigger recovery
|
|
569
|
+
const result = await loader.load(pathToFileURL(targetFile).href, {}, async () => ({}));
|
|
570
|
+
assert.ok(result.source);
|
|
571
|
+
assert.equal(reExtractCalls, 1);
|
|
572
|
+
assert.ok(result.source.includes('y = 2'));
|
|
573
|
+
} finally {
|
|
574
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// T3: tryHoistCycleBreakingImports の hoist 対象欠落を回復する
|
|
579
|
+
test('T3: tryHoistCycleBreakingImports recovers missing hoist target', async () => {
|
|
580
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
581
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t3-${process.pid}-${Date.now()}`);
|
|
582
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
586
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
587
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
588
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
589
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
590
|
+
|
|
591
|
+
const srcFile = path.join(tempDir, 'src.js');
|
|
592
|
+
fs.writeFileSync(srcFile, 'import.meta.require("/$bunfs/root/tgt.js");\n');
|
|
593
|
+
|
|
594
|
+
const tgtFile = path.join(tempDir, 'tgt.js');
|
|
595
|
+
fs.writeFileSync(tgtFile, 'export const target = 1;');
|
|
596
|
+
|
|
597
|
+
let reExtractCalls = 0;
|
|
598
|
+
loader.initialize({
|
|
599
|
+
processOwnedDir: tempDir,
|
|
600
|
+
sourceBin: sourceBin,
|
|
601
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
602
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
603
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
604
|
+
cycleHoists: [{ file: 'src.js', targetModule: 'tgt.js', expectedOccurrences: 1, assertProperties: [] }],
|
|
605
|
+
reExtract: () => {
|
|
606
|
+
reExtractCalls++;
|
|
607
|
+
fs.writeFileSync(tgtFile, 'export const target = 1;');
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Delete target
|
|
612
|
+
fs.unlinkSync(tgtFile);
|
|
613
|
+
|
|
614
|
+
// load() should trigger hoisting and recovery
|
|
615
|
+
const result = await loader.load(pathToFileURL(srcFile).href, {}, async () => ({}));
|
|
616
|
+
assert.ok(result.source);
|
|
617
|
+
assert.equal(reExtractCalls, 1);
|
|
618
|
+
assert.ok(result.source.includes('__bunfsHoisted_'));
|
|
619
|
+
} finally {
|
|
620
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// T4: recoverMissing 直接 — 失敗上限 MAX_CONSEC_FAILURES=3
|
|
625
|
+
test('T4: recoverMissing respects MAX_CONSEC_FAILURES limit of 3', async () => {
|
|
626
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
627
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t4-${process.pid}-${Date.now()}`);
|
|
628
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
632
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
633
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
634
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
635
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
636
|
+
|
|
637
|
+
const missingPath = path.join(tempDir, 'missing.js');
|
|
638
|
+
|
|
639
|
+
let reExtractCalls = 0;
|
|
640
|
+
loader.initialize({
|
|
641
|
+
processOwnedDir: tempDir,
|
|
642
|
+
sourceBin: sourceBin,
|
|
643
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
644
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
645
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
646
|
+
reExtract: () => {
|
|
647
|
+
reExtractCalls++;
|
|
648
|
+
// Do not recreate file - simulate failure
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Call recoverMissing 4 times with advancing time
|
|
653
|
+
const result1 = loader.recoverMissing(missingPath, 0);
|
|
654
|
+
const result2 = loader.recoverMissing(missingPath, 10000);
|
|
655
|
+
const result3 = loader.recoverMissing(missingPath, 20000);
|
|
656
|
+
const result4 = loader.recoverMissing(missingPath, 30000);
|
|
657
|
+
|
|
658
|
+
assert.equal(result1, false);
|
|
659
|
+
assert.equal(result2, false);
|
|
660
|
+
assert.equal(result3, false);
|
|
661
|
+
assert.equal(result4, false);
|
|
662
|
+
assert.equal(reExtractCalls, 3, 'reExtract should be called exactly 3 times (MAX_CONSEC_FAILURES)');
|
|
663
|
+
} finally {
|
|
664
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// T5: recoverMissing 直接 — 連続失敗カウンタは成功でリセット
|
|
669
|
+
test('T5: recoverMissing resets consecutive failures counter on success', async () => {
|
|
670
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
671
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t5-${process.pid}-${Date.now()}`);
|
|
672
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
676
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
677
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
678
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
679
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
680
|
+
|
|
681
|
+
const testPath = path.join(tempDir, 'test.js');
|
|
682
|
+
|
|
683
|
+
let shouldRestore = false;
|
|
684
|
+
loader.initialize({
|
|
685
|
+
processOwnedDir: tempDir,
|
|
686
|
+
sourceBin: sourceBin,
|
|
687
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
688
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
689
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
690
|
+
reExtract: () => {
|
|
691
|
+
if (shouldRestore) {
|
|
692
|
+
fs.writeFileSync(testPath, 'export const z = 3;');
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Attempt 1: recovery fails (no file created)
|
|
698
|
+
shouldRestore = false;
|
|
699
|
+
const r1 = loader.recoverMissing(testPath, 0);
|
|
700
|
+
assert.equal(r1, false);
|
|
701
|
+
|
|
702
|
+
// Attempt 2: recovery succeeds (file created)
|
|
703
|
+
shouldRestore = true;
|
|
704
|
+
const r2 = loader.recoverMissing(testPath, 5000);
|
|
705
|
+
assert.equal(r2, true);
|
|
706
|
+
|
|
707
|
+
// Delete the file again
|
|
708
|
+
fs.unlinkSync(testPath);
|
|
709
|
+
|
|
710
|
+
// Attempt 3: failure again, but counter was reset
|
|
711
|
+
shouldRestore = false;
|
|
712
|
+
const r3 = loader.recoverMissing(testPath, 10000);
|
|
713
|
+
assert.equal(r3, false);
|
|
714
|
+
|
|
715
|
+
// Attempt 4: success again (not yet hit limit)
|
|
716
|
+
shouldRestore = true;
|
|
717
|
+
const r4 = loader.recoverMissing(testPath, 15000);
|
|
718
|
+
assert.equal(r4, true);
|
|
719
|
+
} finally {
|
|
720
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// T6: recoverMissing 直接 — 3s スロットル
|
|
725
|
+
test('T6: recoverMissing throttles re-extraction for 3 seconds', async () => {
|
|
726
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
727
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t6-${process.pid}-${Date.now()}`);
|
|
728
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
732
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
733
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
734
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
735
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
736
|
+
|
|
737
|
+
const missingPath = path.join(tempDir, 'missing.js');
|
|
738
|
+
|
|
739
|
+
let reExtractCalls = 0;
|
|
740
|
+
loader.initialize({
|
|
741
|
+
processOwnedDir: tempDir,
|
|
742
|
+
sourceBin: sourceBin,
|
|
743
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
744
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
745
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
746
|
+
reExtract: () => {
|
|
747
|
+
reExtractCalls++;
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Call at time 10000 (base time)
|
|
752
|
+
loader.recoverMissing(missingPath, 10000);
|
|
753
|
+
assert.equal(reExtractCalls, 1);
|
|
754
|
+
|
|
755
|
+
// Call at time 11000 (only 1s later, < 3s throttle)
|
|
756
|
+
loader.recoverMissing(missingPath, 11000);
|
|
757
|
+
assert.equal(reExtractCalls, 1, 'throttled - should not call reExtract');
|
|
758
|
+
|
|
759
|
+
// Call at time 14000 (4s later, > 3s throttle)
|
|
760
|
+
loader.recoverMissing(missingPath, 14000);
|
|
761
|
+
assert.equal(reExtractCalls, 2);
|
|
762
|
+
} finally {
|
|
763
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// T7: TOCTOU — 2回連続で回復できることを確認
|
|
768
|
+
test('T7: recoverMissing handles repeated deletion and recovery', async () => {
|
|
769
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
770
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t7-${process.pid}-${Date.now()}`);
|
|
771
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
775
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
776
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
777
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
778
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
779
|
+
|
|
780
|
+
const targetFile = path.join(tempDir, 'target.js');
|
|
781
|
+
fs.writeFileSync(targetFile, 'export const x = 1;');
|
|
782
|
+
|
|
783
|
+
let reExtractCalls = 0;
|
|
784
|
+
loader.initialize({
|
|
785
|
+
processOwnedDir: tempDir,
|
|
786
|
+
sourceBin: sourceBin,
|
|
787
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
788
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
789
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
790
|
+
reExtract: (sb, od) => {
|
|
791
|
+
reExtractCalls++;
|
|
792
|
+
fs.writeFileSync(targetFile, 'export const x = 1;');
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// First recovery at time 10000
|
|
797
|
+
fs.unlinkSync(targetFile);
|
|
798
|
+
// Use recoverMissing with explicit time to bypass throttle
|
|
799
|
+
loader.recoverMissing(targetFile, 10000);
|
|
800
|
+
assert.ok(fs.existsSync(targetFile));
|
|
801
|
+
assert.equal(reExtractCalls, 1);
|
|
802
|
+
|
|
803
|
+
// Second recovery at time 14000 (past throttle window)
|
|
804
|
+
fs.unlinkSync(targetFile);
|
|
805
|
+
loader.recoverMissing(targetFile, 14000);
|
|
806
|
+
assert.ok(fs.existsSync(targetFile));
|
|
807
|
+
assert.equal(reExtractCalls, 2);
|
|
808
|
+
} finally {
|
|
809
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// T8 (最重要): real が存在するのに require 失敗 → 再展開しない
|
|
814
|
+
test('T8: recoverMissing does not re-extract when real file exists', async () => {
|
|
815
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
816
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-t8-${process.pid}-${Date.now()}`);
|
|
817
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
821
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
822
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
823
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
824
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
825
|
+
|
|
826
|
+
const realExistsFile = path.join(tempDir, 'real-exists.js');
|
|
827
|
+
// Create a file that exists but would throw MODULE_NOT_FOUND on require
|
|
828
|
+
fs.writeFileSync(realExistsFile, 'throw new Error("internal dependency error");');
|
|
829
|
+
|
|
830
|
+
let reExtractCalls = 0;
|
|
831
|
+
loader.initialize({
|
|
832
|
+
processOwnedDir: tempDir,
|
|
833
|
+
sourceBin: sourceBin,
|
|
834
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
835
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
836
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
837
|
+
reExtract: () => {
|
|
838
|
+
reExtractCalls++;
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// Call recoverMissing with existing file
|
|
843
|
+
const result = loader.recoverMissing(realExistsFile, Date.now());
|
|
844
|
+
assert.equal(result, true, 'should return true for existing file');
|
|
845
|
+
assert.equal(reExtractCalls, 0, 'reExtract should not be called for existing file');
|
|
846
|
+
|
|
847
|
+
// Verify the prelude guards against error-code-based recovery for real files
|
|
848
|
+
const srcFile = path.join(tempDir, 'src.js');
|
|
849
|
+
fs.writeFileSync(srcFile, 'import.meta.require("/$bunfs/root/real-exists.js");');
|
|
850
|
+
const result2 = await loader.load(pathToFileURL(srcFile).href, {}, async () => ({}));
|
|
851
|
+
assert.ok(result2.source);
|
|
852
|
+
// The source should have __bunfsMetaRequireExistsSync guard (not error-code-based)
|
|
853
|
+
assert.ok(result2.source.includes('__bunfsMetaRequireExistsSync'));
|
|
854
|
+
} finally {
|
|
855
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// T8-exec: recoverMissing の no-reextract-when-file-exists を実行確認
|
|
860
|
+
test('T8-exec: recoverMissing returns true immediately for an existing file without calling reExtract, even under repeated calls', async () => {
|
|
861
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
862
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-t8exec-${process.pid}-${Date.now()}`);
|
|
863
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
867
|
+
fs.writeFileSync(sourceBin, 'binary');
|
|
868
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
869
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
870
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
871
|
+
|
|
872
|
+
// 存在する実ファイル (require すれば内部依存 MODULE_NOT_FOUND を投げる想定の中身)
|
|
873
|
+
const realExists = path.join(tempDir, 'has-internal-dep.js');
|
|
874
|
+
fs.writeFileSync(realExists, "module.exports = require('/definitely/not/here.js');");
|
|
875
|
+
|
|
876
|
+
let reExtractCalls = 0;
|
|
877
|
+
loader.initialize({
|
|
878
|
+
processOwnedDir: tempDir,
|
|
879
|
+
sourceBin,
|
|
880
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
881
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
882
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
883
|
+
reExtract: () => { reExtractCalls++; },
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// ファイルが存在する限り、何度呼んでも即 true・再展開ゼロ
|
|
887
|
+
for (let i = 0; i < 5; i++) {
|
|
888
|
+
const r = loader.recoverMissing(realExists, i * 10000);
|
|
889
|
+
assert.equal(r, true, `call ${i} should return true (file exists)`);
|
|
890
|
+
}
|
|
891
|
+
assert.equal(reExtractCalls, 0, 'reExtract must never be called while the target file exists');
|
|
892
|
+
|
|
893
|
+
// 実際に require が内部依存で投げることも確認 (元例外が保持されるべき挙動の裏付け)
|
|
894
|
+
let threw = null;
|
|
895
|
+
try { require(realExists); } catch (e) { threw = e; }
|
|
896
|
+
assert.ok(threw, 'require of the file should throw due to its missing internal dependency');
|
|
897
|
+
assert.equal(reExtractCalls, 0, 'a require-time internal MODULE_NOT_FOUND must not trigger re-extraction');
|
|
898
|
+
} finally {
|
|
899
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// New tests for fs interception functionality
|
|
904
|
+
|
|
905
|
+
test('resolveBunfsPath: non-target paths return null', async () => {
|
|
906
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
907
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-resolve-path-test-${process.pid}-${Date.now()}`);
|
|
908
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
loader.initialize({
|
|
912
|
+
processOwnedDir: tempDir,
|
|
913
|
+
sourceBin: '/dummy/bin',
|
|
914
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
915
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
916
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const result = loader.resolveBunfsPath('/regular/path/file.js');
|
|
920
|
+
assert.equal(result, null);
|
|
921
|
+
} finally {
|
|
922
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test('resolveBunfsPath: rejects path traversal with ..', async () => {
|
|
927
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
928
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-resolve-path-test-${process.pid}-${Date.now()}`);
|
|
929
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
loader.initialize({
|
|
933
|
+
processOwnedDir: tempDir,
|
|
934
|
+
sourceBin: '/dummy/bin',
|
|
935
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
936
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
937
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
assert.throws(
|
|
941
|
+
() => loader.resolveBunfsPath('/$bunfs/root/../../etc/passwd'),
|
|
942
|
+
/rejected specifier/,
|
|
943
|
+
);
|
|
944
|
+
} finally {
|
|
945
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test('resolveBunfsPath: resolves valid /$bunfs/root/ paths correctly', async () => {
|
|
950
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
951
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-resolve-path-test-${process.pid}-${Date.now()}`);
|
|
952
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
953
|
+
const targetFile = path.join(tempDir, 'foo.js');
|
|
954
|
+
fs.writeFileSync(targetFile, 'export const x = 1;');
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
loader.initialize({
|
|
958
|
+
processOwnedDir: tempDir,
|
|
959
|
+
sourceBin: '/dummy/bin',
|
|
960
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
961
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
962
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
const result = loader.resolveBunfsPath('/$bunfs/root/foo.js');
|
|
966
|
+
assert.ok(result);
|
|
967
|
+
assert.equal(result, targetFile);
|
|
968
|
+
} finally {
|
|
969
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
test('syncBuiltinESMExports: fs interception requires it for ESM sync', async () => {
|
|
974
|
+
// installFsBunfsInterception() 自体を、G1/G4 で terra が実機確認した正しい順序
|
|
975
|
+
// (ESM fixture を先に import してバインディング確定 → fs 差替え(sync前)は旧関数 →
|
|
976
|
+
// sync 後は新関数) で検証する。ハンドロールした模擬ではなく実装本体を子プロセスで実行する。
|
|
977
|
+
const { spawnSync } = require('node:child_process');
|
|
978
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-sync-test-${process.pid}-${Date.now()}`);
|
|
979
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
980
|
+
const loaderPath = path.join(__dirname, 'bunfs-esm-loader.mjs');
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
const targetFile = path.join(tempDir, 'target.js');
|
|
984
|
+
fs.writeFileSync(targetFile, 'export const marker = "REAL_CONTENT";');
|
|
985
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
986
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
987
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
988
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
989
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
990
|
+
|
|
991
|
+
// fixture.mjs: ESM named import を先に確立する側 (installFsBunfsInterception より前に
|
|
992
|
+
// dynamic import することで、バインディングが patch 前の状態で確定する)
|
|
993
|
+
const fixturePath = path.join(tempDir, 'fixture.mjs');
|
|
994
|
+
fs.writeFileSync(fixturePath, `
|
|
995
|
+
import { readFileSync } from 'node:fs';
|
|
996
|
+
export function readIt(p) { return readFileSync(p, 'utf8'); }
|
|
997
|
+
`);
|
|
998
|
+
|
|
999
|
+
const testScript = path.join(tempDir, 'test-sync.mjs');
|
|
1000
|
+
fs.writeFileSync(testScript, `
|
|
1001
|
+
const tempDir = ${JSON.stringify(tempDir)};
|
|
1002
|
+
const targetFile = ${JSON.stringify(targetFile)};
|
|
1003
|
+
|
|
1004
|
+
// 1. ESM fixture を先に import (バインディングを patch 前の状態で確定させる)
|
|
1005
|
+
const { readIt } = await import(${JSON.stringify(pathToFileURL(fixturePath).href)});
|
|
1006
|
+
|
|
1007
|
+
// 2. installFsBunfsInterception() 未適用の状態での素の読み込み確認 (対照)
|
|
1008
|
+
const before = readIt(targetFile);
|
|
1009
|
+
if (before !== 'export const marker = "REAL_CONTENT";') {
|
|
1010
|
+
console.error('SETUP_FAILED: fixture cannot read target file before patch');
|
|
1011
|
+
process.exit(1);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// 3. fs を差し替える (installFsBunfsInterception 経由、syncBuiltinESMExports 込み)
|
|
1015
|
+
const loader = await import(${JSON.stringify(pathToFileURL(loaderPath).href)});
|
|
1016
|
+
loader.initialize({
|
|
1017
|
+
processOwnedDir: tempDir,
|
|
1018
|
+
sourceBin: ${JSON.stringify(sourceBin)},
|
|
1019
|
+
childProcessGuardPath: ${JSON.stringify(path.join(tempDir, 'guard.mjs'))},
|
|
1020
|
+
vmGuardPath: ${JSON.stringify(path.join(tempDir, 'vm-guard.mjs'))},
|
|
1021
|
+
wsStubPath: ${JSON.stringify(path.join(tempDir, 'ws-stub.mjs'))},
|
|
1022
|
+
});
|
|
1023
|
+
loader.installFsBunfsInterception();
|
|
1024
|
+
|
|
1025
|
+
// 4. 先に確立した ESM バインディング経由で /$bunfs/root/ パスを読む
|
|
1026
|
+
// → syncBuiltinESMExports() が正しく効いていれば、fixture の readFileSync も
|
|
1027
|
+
// パッチ後の関数を参照し、bunfs パス解決が機能するはず
|
|
1028
|
+
const afterViaBinding = readIt('/$bunfs/root/target.js');
|
|
1029
|
+
if (afterViaBinding !== 'export const marker = "REAL_CONTENT";') {
|
|
1030
|
+
console.error('SYNC_FAILED: pre-bound ESM readFileSync did not pick up the fs interception patch');
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
console.log('SYNC_OK');
|
|
1034
|
+
process.exit(0);
|
|
1035
|
+
`);
|
|
1036
|
+
|
|
1037
|
+
const result = spawnSync('node', [testScript], { encoding: 'utf8' });
|
|
1038
|
+
assert.equal(result.status, 0, `Script failed (status=${result.status}): stdout=${result.stdout} stderr=${result.stderr}`);
|
|
1039
|
+
assert.ok(result.stdout.includes('SYNC_OK'), `expected SYNC_OK marker, got: ${result.stdout}`);
|
|
1040
|
+
} finally {
|
|
1041
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test('fs.readFile callback mode: resolves /$bunfs/root/ paths', async () => {
|
|
1046
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1047
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-fs-readfile-${process.pid}-${Date.now()}`);
|
|
1048
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1049
|
+
const testFile = path.join(tempDir, 'test.js');
|
|
1050
|
+
const testContent = 'export const y = 42;';
|
|
1051
|
+
fs.writeFileSync(testFile, testContent);
|
|
1052
|
+
|
|
1053
|
+
try {
|
|
1054
|
+
loader.initialize({
|
|
1055
|
+
processOwnedDir: tempDir,
|
|
1056
|
+
sourceBin: '/dummy/bin',
|
|
1057
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1058
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1059
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
loader.installFsBunfsInterception();
|
|
1063
|
+
|
|
1064
|
+
// After interception is installed, fs.readFile should resolve bunfs paths
|
|
1065
|
+
const testFsModule = require('node:fs');
|
|
1066
|
+
let callbackCalled = false;
|
|
1067
|
+
let readData = null;
|
|
1068
|
+
|
|
1069
|
+
testFsModule.readFile('/$bunfs/root/test.js', 'utf8', (err, data) => {
|
|
1070
|
+
callbackCalled = true;
|
|
1071
|
+
if (!err) {
|
|
1072
|
+
readData = data;
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Give callback time to execute
|
|
1077
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1078
|
+
assert.equal(callbackCalled, true);
|
|
1079
|
+
assert.equal(readData, testContent);
|
|
1080
|
+
} finally {
|
|
1081
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
test('fs.readFile with options: resolves /$bunfs/root/ paths', async () => {
|
|
1086
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1087
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-fs-readfile-opts-${process.pid}-${Date.now()}`);
|
|
1088
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1089
|
+
const testFile = path.join(tempDir, 'test.txt');
|
|
1090
|
+
const testContent = 'Hello World';
|
|
1091
|
+
fs.writeFileSync(testFile, testContent);
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
loader.initialize({
|
|
1095
|
+
processOwnedDir: tempDir,
|
|
1096
|
+
sourceBin: '/dummy/bin',
|
|
1097
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1098
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1099
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
loader.installFsBunfsInterception();
|
|
1103
|
+
|
|
1104
|
+
const testFsModule = require('node:fs');
|
|
1105
|
+
let callbackCalled = false;
|
|
1106
|
+
let readData = null;
|
|
1107
|
+
|
|
1108
|
+
testFsModule.readFile('/$bunfs/root/test.txt', { encoding: 'utf8' }, (err, data) => {
|
|
1109
|
+
callbackCalled = true;
|
|
1110
|
+
if (!err) {
|
|
1111
|
+
readData = data;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1116
|
+
assert.equal(callbackCalled, true);
|
|
1117
|
+
assert.equal(readData, testContent);
|
|
1118
|
+
} finally {
|
|
1119
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
test('fs.readFileSync: non-bunfs paths unchanged after interception', async () => {
|
|
1124
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1125
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-fs-normal-${process.pid}-${Date.now()}`);
|
|
1126
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1127
|
+
const normalFile = path.join(tempDir, 'normal.txt');
|
|
1128
|
+
const normalContent = 'Normal File Content';
|
|
1129
|
+
fs.writeFileSync(normalFile, normalContent);
|
|
1130
|
+
|
|
1131
|
+
try {
|
|
1132
|
+
loader.initialize({
|
|
1133
|
+
processOwnedDir: tempDir,
|
|
1134
|
+
sourceBin: '/dummy/bin',
|
|
1135
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1136
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1137
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
loader.installFsBunfsInterception();
|
|
1141
|
+
|
|
1142
|
+
const testFsModule = require('node:fs');
|
|
1143
|
+
const data = testFsModule.readFileSync(normalFile, 'utf8');
|
|
1144
|
+
assert.equal(data, normalContent);
|
|
1145
|
+
} finally {
|
|
1146
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
test('fs interception: symlink escape detection', async () => {
|
|
1151
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1152
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-symlink-${process.pid}-${Date.now()}`);
|
|
1153
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1154
|
+
const externalDir = path.join(os.tmpdir(), `bunfs-external-${process.pid}-${Date.now()}`);
|
|
1155
|
+
fs.mkdirSync(externalDir, { recursive: true });
|
|
1156
|
+
const externalFile = path.join(externalDir, 'external.txt');
|
|
1157
|
+
fs.writeFileSync(externalFile, 'External');
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
loader.initialize({
|
|
1161
|
+
processOwnedDir: tempDir,
|
|
1162
|
+
sourceBin: '/dummy/bin',
|
|
1163
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1164
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1165
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
loader.installFsBunfsInterception();
|
|
1169
|
+
|
|
1170
|
+
// Try to create a symlink (may fail on some platforms)
|
|
1171
|
+
const symlinkPath = path.join(tempDir, 'escape.txt');
|
|
1172
|
+
try {
|
|
1173
|
+
fs.symlinkSync(externalFile, symlinkPath);
|
|
1174
|
+
} catch (e) {
|
|
1175
|
+
// Skip test if symlinks not supported
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const testFsModule = require('node:fs');
|
|
1180
|
+
assert.throws(
|
|
1181
|
+
() => testFsModule.readFileSync('/$bunfs/root/escape.txt'),
|
|
1182
|
+
/escapes owned dir via symlink/,
|
|
1183
|
+
);
|
|
1184
|
+
} finally {
|
|
1185
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1186
|
+
fs.rmSync(externalDir, { recursive: true, force: true });
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test('installFsBunfsInterception: idempotent (multiple calls are safe)', async () => {
|
|
1191
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1192
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-idempotent-${process.pid}-${Date.now()}`);
|
|
1193
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1194
|
+
const testFile = path.join(tempDir, 'test.js');
|
|
1195
|
+
fs.writeFileSync(testFile, 'export const z = 1;');
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
loader.initialize({
|
|
1199
|
+
processOwnedDir: tempDir,
|
|
1200
|
+
sourceBin: '/dummy/bin',
|
|
1201
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1202
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1203
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// Call installFsBunfsInterception multiple times
|
|
1207
|
+
loader.installFsBunfsInterception();
|
|
1208
|
+
loader.installFsBunfsInterception();
|
|
1209
|
+
loader.installFsBunfsInterception();
|
|
1210
|
+
|
|
1211
|
+
// Verify fs still works
|
|
1212
|
+
const testFsModule = require('node:fs');
|
|
1213
|
+
const data = testFsModule.readFileSync(testFile, 'utf8');
|
|
1214
|
+
assert.ok(data.includes('z = 1'));
|
|
1215
|
+
} finally {
|
|
1216
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
test('fs interception: rollback on partial failure leaves FS_PATCHED false (retry succeeds)', async () => {
|
|
1221
|
+
// G1で確定した設計: syncBuiltinESMExports() が失敗した場合、3関数を元に戻し
|
|
1222
|
+
// FS_PATCHED は立てない。次回呼出しで再試行できることを外部挙動で証明する
|
|
1223
|
+
// (private 変数を直接読まず、「1回目は throw して起こす失敗後、2回目の呼出しが
|
|
1224
|
+
// 実際にパッチを完了する」ことで FS_PATCHED===false だったことを証明する)。
|
|
1225
|
+
const { spawnSync } = require('node:child_process');
|
|
1226
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-rollback-${process.pid}-${Date.now()}`);
|
|
1227
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1228
|
+
const loaderPath = path.join(__dirname, 'bunfs-esm-loader.mjs');
|
|
1229
|
+
|
|
1230
|
+
try {
|
|
1231
|
+
const targetFile = path.join(tempDir, 'target.js');
|
|
1232
|
+
fs.writeFileSync(targetFile, 'export const marker = "REAL_CONTENT";');
|
|
1233
|
+
fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
|
|
1234
|
+
fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
|
|
1235
|
+
fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
|
|
1236
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
1237
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
1238
|
+
const normalFile = path.join(tempDir, 'normal.txt');
|
|
1239
|
+
fs.writeFileSync(normalFile, 'NORMAL_CONTENT');
|
|
1240
|
+
|
|
1241
|
+
const testScript = path.join(tempDir, 'test-rollback.mjs');
|
|
1242
|
+
fs.writeFileSync(testScript, `
|
|
1243
|
+
import { createRequire } from 'node:module';
|
|
1244
|
+
const require = createRequire(import.meta.url);
|
|
1245
|
+
|
|
1246
|
+
const tempDir = ${JSON.stringify(tempDir)};
|
|
1247
|
+
const normalFile = ${JSON.stringify(normalFile)};
|
|
1248
|
+
// ESM namespace ('node:module' の import 経由の名前空間) は読み取り専用のため、
|
|
1249
|
+
// installFsBunfsInterception 自体と同じく CJS 側 (require) を可変対象として差し替える。
|
|
1250
|
+
const nodeModuleCjs = require('node:module');
|
|
1251
|
+
const origFsMod = require('node:fs');
|
|
1252
|
+
const origReadFileSync = origFsMod.readFileSync;
|
|
1253
|
+
const origReadFile = origFsMod.readFile;
|
|
1254
|
+
const origPromisesReadFile = origFsMod.promises.readFile;
|
|
1255
|
+
const realSync = nodeModuleCjs.syncBuiltinESMExports;
|
|
1256
|
+
|
|
1257
|
+
// 1回目のみ throw するモックへ差し替え。差し替え自体を ESM 側にも伝播させるため
|
|
1258
|
+
// 一度だけ本物の syncBuiltinESMExports を呼んでおく (この呼出し自体は失敗しない)。
|
|
1259
|
+
let syncCallCount = 0;
|
|
1260
|
+
nodeModuleCjs.syncBuiltinESMExports = () => {
|
|
1261
|
+
syncCallCount++;
|
|
1262
|
+
if (syncCallCount === 1) {
|
|
1263
|
+
throw new Error('forced syncBuiltinESMExports failure (test)');
|
|
1264
|
+
}
|
|
1265
|
+
return realSync();
|
|
1266
|
+
};
|
|
1267
|
+
realSync();
|
|
1268
|
+
|
|
1269
|
+
const loader = await import(${JSON.stringify(pathToFileURL(loaderPath).href)});
|
|
1270
|
+
loader.initialize({
|
|
1271
|
+
processOwnedDir: tempDir,
|
|
1272
|
+
sourceBin: ${JSON.stringify(sourceBin)},
|
|
1273
|
+
childProcessGuardPath: ${JSON.stringify(path.join(tempDir, 'guard.mjs'))},
|
|
1274
|
+
vmGuardPath: ${JSON.stringify(path.join(tempDir, 'vm-guard.mjs'))},
|
|
1275
|
+
wsStubPath: ${JSON.stringify(path.join(tempDir, 'ws-stub.mjs'))},
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// 1回目: syncBuiltinESMExports が throw するため installFsBunfsInterception も throw するはず
|
|
1279
|
+
let firstThrew = false;
|
|
1280
|
+
try {
|
|
1281
|
+
loader.installFsBunfsInterception();
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
firstThrew = true;
|
|
1284
|
+
}
|
|
1285
|
+
if (!firstThrew) {
|
|
1286
|
+
console.error('EXPECTED_THROW_MISSING: first installFsBunfsInterception() call did not throw');
|
|
1287
|
+
process.exit(1);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// ロールバック確認: 3関数が元の参照に戻っているか (通常ファイル読み込みが正常動作することで確認)
|
|
1291
|
+
const fsMod = require('node:fs');
|
|
1292
|
+
if (fsMod.readFileSync !== origReadFileSync || fsMod.readFile !== origReadFile || fsMod.promises.readFile !== origPromisesReadFile) {
|
|
1293
|
+
console.error('ROLLBACK_FAILED: fs functions were not restored to originals after failure');
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
const normalContent = fsMod.readFileSync(normalFile, 'utf8');
|
|
1297
|
+
if (normalContent !== 'NORMAL_CONTENT') {
|
|
1298
|
+
console.error('ROLLBACK_BROKEN_READ: normal file read broken after rollback');
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// 2回目: モックは以後成功するため、FS_PATCHED が false のままなら今度は成功するはず
|
|
1303
|
+
let secondThrew = false;
|
|
1304
|
+
try {
|
|
1305
|
+
loader.installFsBunfsInterception();
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
secondThrew = true;
|
|
1308
|
+
console.error('SECOND_CALL_THREW: ' + e.message);
|
|
1309
|
+
}
|
|
1310
|
+
if (secondThrew) {
|
|
1311
|
+
console.error('FS_PATCHED_STUCK_TRUE_OR_RETRY_BLOCKED: second call should have succeeded');
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
console.log('ROLLBACK_AND_RETRY_OK');
|
|
1316
|
+
process.exit(0);
|
|
1317
|
+
`);
|
|
1318
|
+
|
|
1319
|
+
const result = spawnSync('node', [testScript], { encoding: 'utf8' });
|
|
1320
|
+
assert.equal(result.status, 0, `Script failed (status=${result.status}): stdout=${result.stdout} stderr=${result.stderr}`);
|
|
1321
|
+
assert.ok(result.stdout.includes('ROLLBACK_AND_RETRY_OK'), `expected ROLLBACK_AND_RETRY_OK marker, got: stdout=${result.stdout} stderr=${result.stderr}`);
|
|
1322
|
+
} finally {
|
|
1323
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
test('recoverMissing integration: fs interception collaborates with recovery', async () => {
|
|
1328
|
+
const loader = await import('./bunfs-esm-loader.mjs');
|
|
1329
|
+
const tempDir = path.join(os.tmpdir(), `bunfs-recovery-integration-${process.pid}-${Date.now()}`);
|
|
1330
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1331
|
+
const targetFile = path.join(tempDir, 'recovered.js');
|
|
1332
|
+
fs.writeFileSync(targetFile, 'export const recovered = true;');
|
|
1333
|
+
const sourceBin = path.join(tempDir, 'bin');
|
|
1334
|
+
fs.writeFileSync(sourceBin, 'binary content');
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
let reExtractCalls = 0;
|
|
1338
|
+
loader.initialize({
|
|
1339
|
+
processOwnedDir: tempDir,
|
|
1340
|
+
sourceBin: sourceBin,
|
|
1341
|
+
childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
|
|
1342
|
+
vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
|
|
1343
|
+
wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
|
|
1344
|
+
reExtract: (sb, od) => {
|
|
1345
|
+
reExtractCalls++;
|
|
1346
|
+
fs.writeFileSync(targetFile, 'export const recovered = true;');
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
loader.installFsBunfsInterception();
|
|
1351
|
+
|
|
1352
|
+
// Delete the file
|
|
1353
|
+
fs.unlinkSync(targetFile);
|
|
1354
|
+
|
|
1355
|
+
// Try to read via fs - should trigger recovery
|
|
1356
|
+
const testFsModule = require('node:fs');
|
|
1357
|
+
const data = testFsModule.readFileSync('/$bunfs/root/recovered.js', 'utf8');
|
|
1358
|
+
assert.ok(data.includes('recovered'));
|
|
1359
|
+
assert.equal(reExtractCalls, 1);
|
|
1360
|
+
} finally {
|
|
1361
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1362
|
+
}
|
|
1363
|
+
});
|