@bash0816/claude-code 2.1.245 → 2.1.251

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.
@@ -33,15 +33,15 @@ test('resolve() handles child_process and node:child_process specifiers', async
33
33
  wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
34
34
  });
35
35
 
36
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
36
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
37
37
 
38
38
  // child_process should resolve to childProcessGuardPath
39
- const result1 = await loader.resolve('child_process', {}, nextResolve);
39
+ const result1 = loader.resolve('child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
40
40
  assert.ok(result1.url.includes(guardPath));
41
41
  assert.equal(result1.shortCircuit, true);
42
42
 
43
43
  // node:child_process should also resolve to childProcessGuardPath
44
- const result2 = await loader.resolve('node:child_process', {}, nextResolve);
44
+ const result2 = loader.resolve('node:child_process', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
45
45
  assert.ok(result2.url.includes(guardPath));
46
46
  assert.equal(result2.shortCircuit, true);
47
47
  } finally {
@@ -65,15 +65,15 @@ test('resolve() handles vm and node:vm specifiers', async () => {
65
65
  wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
66
66
  });
67
67
 
68
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
68
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
69
69
 
70
70
  // vm should resolve to vmGuardPath
71
- const result1 = await loader.resolve('vm', {}, nextResolve);
71
+ const result1 = loader.resolve('vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
72
72
  assert.ok(result1.url.includes(vmGuardPath));
73
73
  assert.equal(result1.shortCircuit, true);
74
74
 
75
75
  // node:vm should also resolve to vmGuardPath
76
- const result2 = await loader.resolve('node:vm', {}, nextResolve);
76
+ const result2 = loader.resolve('node:vm', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
77
77
  assert.ok(result2.url.includes(vmGuardPath));
78
78
  assert.equal(result2.shortCircuit, true);
79
79
  } finally {
@@ -97,9 +97,9 @@ test('resolve() handles ws specifier', async () => {
97
97
  wsStubPath: wsStubPath,
98
98
  });
99
99
 
100
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
100
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
101
101
 
102
- const result = await loader.resolve('ws', {}, nextResolve);
102
+ const result = loader.resolve('ws', { parentURL: pathToFileURL(path.join(tempDir, 'dummy-chunk.js')).href }, nextResolve);
103
103
  assert.ok(result.url.includes(wsStubPath));
104
104
  assert.equal(result.shortCircuit, true);
105
105
  } finally {
@@ -150,9 +150,9 @@ test('resolve() rejects path traversal with ..', async () => {
150
150
  wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
151
151
  });
152
152
 
153
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
153
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
154
154
 
155
- await assert.rejects(
155
+ assert.throws(
156
156
  () => loader.resolve('/$bunfs/root/../../etc/passwd', {}, nextResolve),
157
157
  /rejected specifier|escapes/,
158
158
  );
@@ -175,9 +175,9 @@ test('resolve() rejects absolute paths', async () => {
175
175
  wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
176
176
  });
177
177
 
178
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
178
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
179
179
 
180
- await assert.rejects(
180
+ assert.throws(
181
181
  () => loader.resolve('/$bunfs/root//etc/passwd', {}, nextResolve),
182
182
  /rejected specifier|escapes/,
183
183
  );
@@ -200,9 +200,9 @@ test('resolve() throws error for missing extracted module', async () => {
200
200
  wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
201
201
  });
202
202
 
203
- const nextResolve = async (spec, ctx) => ({ url: `unresolved:${spec}` });
203
+ const nextResolve = (spec, ctx) => ({ url: `unresolved:${spec}` });
204
204
 
205
- await assert.rejects(
205
+ assert.throws(
206
206
  () => loader.resolve('/$bunfs/root/nonexistent.js', {}, nextResolve),
207
207
  /missing extracted module/,
208
208
  );
@@ -327,3 +327,1037 @@ test('load() calls nextLoad for URLs outside processOwnedDir', async () => {
327
327
  fs.rmSync(tempDir, { recursive: true, force: true });
328
328
  }
329
329
  });
330
+
331
+ test('load() hoists the cycle-breaking import.meta.require call in chunk-vmw9kxhv.js', async () => {
332
+ const loader = await import('./bunfs-esm-loader.mjs');
333
+ const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-hoist-test-${process.pid}-${Date.now()}`);
334
+ fs.mkdirSync(tempDir, { recursive: true });
335
+
336
+ fs.writeFileSync(path.join(tempDir, 'chunk-y0jj307t.js'), 'export const daemonColdStartGbDefault = () => "fixture";\n');
337
+ const targetFile = path.join(tempDir, 'chunk-vmw9kxhv.js');
338
+ const sourceCode = 'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n';
339
+ fs.writeFileSync(targetFile, sourceCode);
340
+
341
+ try {
342
+ loader.initialize({
343
+ processOwnedDir: tempDir,
344
+ sourceBin: '/dummy/bin',
345
+ childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
346
+ vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
347
+ wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
348
+ cycleHoists: [{ file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] }],
349
+ });
350
+
351
+ const fileUrl = pathToFileURL(targetFile).href;
352
+ const result = await loader.load(fileUrl, {}, async () => ({ source: 'fallback' }));
353
+
354
+ assert.equal(result.format, 'module');
355
+ assert.ok(result.source.includes('import * as __bunfsHoisted_0 from'));
356
+ assert.ok(!result.source.includes('var O9=import.meta.require('));
357
+ assert.ok(result.source.includes('var O9=__bunfsHoisted_0'));
358
+ assert.equal(result.shortCircuit, true);
359
+ } finally {
360
+ fs.rmSync(tempDir, { recursive: true, force: true });
361
+ }
362
+ });
363
+
364
+ test('import.meta.require resolves /$bunfs/root/ specifiers via loader integration', async () => {
365
+ const { registerHooks } = await import('node:module');
366
+ const loader = await import('./bunfs-esm-loader.mjs');
367
+ const tempDir = path.join(os.tmpdir(), `bunfs-esm-loader-integration-${process.pid}-${Date.now()}`);
368
+ fs.mkdirSync(tempDir, { recursive: true });
369
+
370
+ try {
371
+ // Create guard files
372
+ fs.writeFileSync(path.join(tempDir, 'guard.mjs'), 'export default {};');
373
+ fs.writeFileSync(path.join(tempDir, 'vm-guard.mjs'), 'export default {};');
374
+ fs.writeFileSync(path.join(tempDir, 'ws-stub.mjs'), 'export default {};');
375
+
376
+ // Create ESM fixture that will be required via import.meta.require (normal case).
377
+ // PROCESS_OWNED_DIR only ever contains genuine ESM chunk files extracted from the
378
+ // Bun esm-chunked bundle (verified against a real 2.1.248 extraction: 0 of 1768
379
+ // chunk files are CommonJS), so load() always returns format: 'module' for this dir.
380
+ fs.writeFileSync(path.join(tempDir, 'foo.js'), 'export const value = 42;');
381
+
382
+ // Create ESM files for each test scenario
383
+ const okCallerPath = path.join(tempDir, 'ok-caller.mjs');
384
+ fs.writeFileSync(okCallerPath, 'export const result = import.meta.require("/$bunfs/root/foo.js").value;\n');
385
+
386
+ const traversalCallerPath = path.join(tempDir, 'traversal-caller.mjs');
387
+ fs.writeFileSync(traversalCallerPath, 'import.meta.require("/$bunfs/root/../../etc/passwd");\n');
388
+
389
+ const missingCallerPath = path.join(tempDir, 'missing-caller.mjs');
390
+ fs.writeFileSync(missingCallerPath, 'import.meta.require("/$bunfs/root/nonexistent.js");\n');
391
+
392
+ // Register loader (only once) with data
393
+ const sourceBin = path.join(tempDir, 'dummy-bin');
394
+ fs.writeFileSync(sourceBin, '#!/bin/false');
395
+
396
+ loader.initialize({
397
+ processOwnedDir: tempDir,
398
+ sourceBin: sourceBin,
399
+ childProcessGuardPath: path.join(tempDir, 'guard.mjs'),
400
+ vmGuardPath: path.join(tempDir, 'vm-guard.mjs'),
401
+ wsStubPath: path.join(tempDir, 'ws-stub.mjs'),
402
+ cycleHoists: [
403
+ { file: 'chunk-vmw9kxhv.js', targetModule: 'chunk-y0jj307t.js', expectedOccurrences: 1, assertProperties: [] },
404
+ ],
405
+ });
406
+ registerHooks({ resolve: loader.resolve, load: loader.load });
407
+
408
+ // Test 1: Normal case - should load and resolve correctly
409
+ const okModule = await import(pathToFileURL(okCallerPath).href);
410
+ assert.equal(okModule.result, 42);
411
+
412
+ // Test 2: Path traversal rejection - should throw error
413
+ await assert.rejects(
414
+ () => import(pathToFileURL(traversalCallerPath).href),
415
+ /rejected specifier|escapes/,
416
+ );
417
+
418
+ // Test 3: Missing module rejection - should throw error
419
+ await assert.rejects(
420
+ () => import(pathToFileURL(missingCallerPath).href),
421
+ /missing extracted module/,
422
+ );
423
+
424
+ // Cycle regression proof: a generic sync-require-into-in-flight-static-import cycle
425
+ // must throw ERR_REQUIRE_CYCLE_MODULE when NOT hoisted (proves our understanding of
426
+ // the bug mechanism is correct, independent of the real chunk-y0jj307t.js file).
427
+ fs.writeFileSync(
428
+ path.join(tempDir, 'chunk-cycle-demo-target.js'),
429
+ 'import "/$bunfs/root/chunk-vmw9kxhv-a.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
430
+ );
431
+ fs.writeFileSync(
432
+ path.join(tempDir, 'chunk-vmw9kxhv-a.js'),
433
+ 'var O9X=import.meta.require("/$bunfs/root/chunk-cycle-demo-target.js");\nexport const value = O9X;\n',
434
+ );
435
+ await assert.rejects(
436
+ () => import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv-a.js')).href),
437
+ (err) => {
438
+ assert.equal(err.code, 'ERR_REQUIRE_CYCLE_MODULE');
439
+ return true;
440
+ },
441
+ );
442
+
443
+ // Cycle fix proof: the real chunk-vmw9kxhv.js / chunk-y0jj307t.js pair (exact filenames
444
+ // and declaration text that tryHoistCycleBreakingImport() targets) must resolve cleanly
445
+ // once hoisting is applied, and the hoisted namespace's property access must work.
446
+ fs.writeFileSync(
447
+ path.join(tempDir, 'chunk-y0jj307t.js'),
448
+ 'import "/$bunfs/root/chunk-vmw9kxhv.js";\nexport const daemonColdStartGbDefault = () => "fixture";\n',
449
+ );
450
+ fs.writeFileSync(
451
+ path.join(tempDir, 'chunk-vmw9kxhv.js'),
452
+ 'var O9=import.meta.require("/$bunfs/root/chunk-y0jj307t.js");\nexport const value = O9.daemonColdStartGbDefault();\n',
453
+ );
454
+ const hoistedModule = await import(pathToFileURL(path.join(tempDir, 'chunk-vmw9kxhv.js')).href);
455
+ assert.equal(hoistedModule.value, 'fixture');
456
+
457
+ fs.writeFileSync(path.join(tempDir, 'doc.md'), '# Hello\nSome markdown text.\n');
458
+ fs.writeFileSync(
459
+ path.join(tempDir, 'md-caller.mjs'),
460
+ 'export const result = import.meta.require("/$bunfs/root/doc.md");\n',
461
+ );
462
+ const mdModule = await import(pathToFileURL(path.join(tempDir, 'md-caller.mjs')).href);
463
+ assert.equal(typeof mdModule.result, 'string');
464
+ assert.equal(mdModule.result, '# Hello\nSome markdown text.\n');
465
+
466
+ fs.writeFileSync(path.join(tempDir, 'note.txt'), 'plain text content');
467
+ fs.writeFileSync(
468
+ path.join(tempDir, 'txt-caller.mjs'),
469
+ 'export const result = import.meta.require("/$bunfs/root/note.txt");\n',
470
+ );
471
+ const txtModule = await import(pathToFileURL(path.join(tempDir, 'txt-caller.mjs')).href);
472
+ assert.equal(typeof txtModule.result, 'string');
473
+ assert.equal(txtModule.result, 'plain text content');
474
+
475
+ fs.writeFileSync(
476
+ path.join(tempDir, 'chunk-alias.js'),
477
+ 'export const ee = import.meta.require;\n',
478
+ );
479
+ fs.writeFileSync(
480
+ path.join(tempDir, 'alias-caller.mjs'),
481
+ 'import { ee } from "/$bunfs/root/chunk-alias.js";\nexport const result = ee("/$bunfs/root/doc.md");\n',
482
+ );
483
+ const aliasModule = await import(pathToFileURL(path.join(tempDir, 'alias-caller.mjs')).href);
484
+ assert.equal(aliasModule.result, '# Hello\nSome markdown text.\n');
485
+ } finally {
486
+ fs.rmSync(tempDir, { recursive: true, force: true });
487
+ }
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
+ });