@usebruno/js 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
2
2
  const {
3
3
  formatErrorWithContext,
4
+ formatErrorWithContextV2,
4
5
  findScriptBlockStartLine,
6
+ findScriptBlockEndLine,
5
7
  findYmlScriptBlockStartLine,
8
+ findYmlScriptBlockEndLine,
6
9
  adjustLineNumber,
7
10
  parseStackTrace,
8
- parseErrorLocation
11
+ parseErrorLocation,
12
+ getSourceContextFromContent,
13
+ adjustStackTrace,
14
+ buildStackFromCallSites
9
15
  } = require('./error-formatter');
10
16
  const fs = require('fs');
11
17
  const path = require('path');
@@ -154,6 +160,31 @@ describe('Error Formatter', () => {
154
160
  });
155
161
  });
156
162
 
163
+ describe('findScriptBlockEndLine', () => {
164
+ it('should find last content line for each block type', () => {
165
+ expect(findScriptBlockEndLine(bruFilePath, 'pre-request')).toBe(15);
166
+ expect(findScriptBlockEndLine(bruFilePath, 'post-response')).toBe(21);
167
+ expect(findScriptBlockEndLine(bruFilePath, 'test')).toBe(30);
168
+ });
169
+
170
+ it('should return null for empty block', () => {
171
+ const emptyBlockPath = path.join(testDir, 'empty.bru');
172
+ fs.writeFileSync(emptyBlockPath, 'script:pre-request {\n}');
173
+ expect(findScriptBlockEndLine(emptyBlockPath, 'pre-request')).toBeNull();
174
+ });
175
+
176
+ it('should return null for missing block', () => {
177
+ const noBlockPath = path.join(testDir, 'no-block.bru');
178
+ fs.writeFileSync(noBlockPath, 'meta {\n name: test\n}');
179
+ expect(findScriptBlockEndLine(noBlockPath, 'pre-request')).toBeNull();
180
+ });
181
+
182
+ it('should return null for non-.bru files', () => {
183
+ expect(findScriptBlockEndLine('/some/file.js', 'pre-request')).toBeNull();
184
+ expect(findScriptBlockEndLine(ymlFilePath, 'pre-request')).toBeNull();
185
+ });
186
+ });
187
+
157
188
  describe('findYmlScriptBlockStartLine', () => {
158
189
  it('should find each block type in .yml files', () => {
159
190
  expect(findYmlScriptBlockStartLine(ymlFilePath, 'pre-request')).toBe(8);
@@ -173,6 +204,36 @@ describe('Error Formatter', () => {
173
204
  });
174
205
  });
175
206
 
207
+ describe('findYmlScriptBlockEndLine', () => {
208
+ it('should find last content line for each block type in runtime.scripts', () => {
209
+ expect(findYmlScriptBlockEndLine(ymlFilePath, 'pre-request')).toBe(9);
210
+ expect(findYmlScriptBlockEndLine(ymlFilePath, 'post-response')).toBe(13);
211
+ expect(findYmlScriptBlockEndLine(ymlFilePath, 'test')).toBe(18);
212
+ });
213
+
214
+ it('should find last content line in collection yml (request.scripts)', () => {
215
+ expect(findYmlScriptBlockEndLine(collectionYmlPath, 'pre-request')).toBe(8);
216
+ expect(findYmlScriptBlockEndLine(collectionYmlPath, 'test')).toBe(13);
217
+ });
218
+
219
+ it('should return null for missing block', () => {
220
+ const noRuntimePath = path.join(testDir, 'no-runtime.yml');
221
+ fs.writeFileSync(noRuntimePath, 'info:\n name: simple\n version: "1"\n');
222
+ expect(findYmlScriptBlockEndLine(noRuntimePath, 'pre-request')).toBeNull();
223
+ });
224
+
225
+ it('should return null for non-.yml files', () => {
226
+ expect(findYmlScriptBlockEndLine('/some/file.js', 'pre-request')).toBeNull();
227
+ expect(findYmlScriptBlockEndLine(bruFilePath, 'pre-request')).toBeNull();
228
+ });
229
+
230
+ it('should return null for invalid YAML', () => {
231
+ const invalidYmlPath = path.join(testDir, 'invalid.yml');
232
+ fs.writeFileSync(invalidYmlPath, ':\n - :\n bad: [unclosed');
233
+ expect(findYmlScriptBlockEndLine(invalidYmlPath, 'pre-request')).toBeNull();
234
+ });
235
+ });
236
+
176
237
  describe('adjustLineNumber', () => {
177
238
  it('should adjust QuickJS lines for .bru files', () => {
178
239
  // VM line - offset(9) = scriptLine → blockStart + scriptLine - 1
@@ -385,4 +446,625 @@ describe('Error Formatter', () => {
385
446
  expect(formatErrorWithContext(error)).toContain('Test error');
386
447
  });
387
448
  });
449
+
450
+ describe('formatErrorWithContextV2', () => {
451
+ const makeCallSiteError = (filePath, line, message = 'test error', name = 'Error') => {
452
+ const error = new Error(message);
453
+ error.name = name;
454
+ error.__callSites = [{ filePath, line, column: 1 }];
455
+ error.stack = `${name}: ${message}\n at Object.<anonymous> (${filePath}:${line}:1)`;
456
+ return error;
457
+ };
458
+
459
+ const makeQuickJSError = (filePath, line, message = 'test error', name = 'Error') => {
460
+ const error = new Error(message);
461
+ error.name = name;
462
+ error.__isQuickJS = true;
463
+ error.stack = `${name}: ${message}\n at <anonymous> (${filePath}:${line})`;
464
+ return error;
465
+ };
466
+
467
+ let consoleSpy;
468
+
469
+ beforeEach(() => {
470
+ consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
471
+ });
472
+
473
+ afterEach(() => {
474
+ consoleSpy.mockRestore();
475
+ });
476
+
477
+ describe('null/falsy error input', () => {
478
+ it('should return null for null error', () => {
479
+ expect(formatErrorWithContextV2(null, 'pre-request')).toBeNull();
480
+ });
481
+
482
+ it('should return null for undefined error', () => {
483
+ expect(formatErrorWithContextV2(undefined, 'pre-request')).toBeNull();
484
+ });
485
+
486
+ it('should return null for error without parseable location', () => {
487
+ const error = new Error('no location');
488
+ error.stack = '';
489
+ expect(formatErrorWithContextV2(error, 'pre-request')).toBeNull();
490
+ });
491
+ });
492
+
493
+ describe('.bru file errors', () => {
494
+ it('should produce correct block-relative line numbers for pre-request (NodeVM)', () => {
495
+ const error = makeCallSiteError(bruFilePath, 3, 'token is not defined', 'ReferenceError');
496
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
497
+
498
+ expect(result).not.toBeNull();
499
+ expect(result.errorType).toBe('ReferenceError');
500
+ expect(result.errorLine).toBe(1);
501
+ expect(result.filePath).toBe('test.bru');
502
+ expect(result.lines.length).toBeGreaterThan(0);
503
+ expect(result.lines.every((l) => l.lineNumber >= 1)).toBe(true);
504
+ });
505
+
506
+ it('should produce correct block-relative line numbers for post-response (NodeVM)', () => {
507
+ const error = makeCallSiteError(bruFilePath, 4, 'data is not defined', 'ReferenceError');
508
+ const result = formatErrorWithContextV2(error, 'post-response', null, testDir);
509
+
510
+ expect(result).not.toBeNull();
511
+ expect(result.errorLine).toBe(2);
512
+ expect(result.filePath).toBe('test.bru');
513
+ });
514
+
515
+ it('should produce correct block-relative line numbers for tests (NodeVM)', () => {
516
+ const error = makeCallSiteError(bruFilePath, 3, 'assertion failed');
517
+ const result = formatErrorWithContextV2(error, 'test', null, testDir);
518
+
519
+ expect(result).not.toBeNull();
520
+ expect(result.errorLine).toBe(1);
521
+ });
522
+
523
+ it('should handle QuickJS errors with correct offset', () => {
524
+ const error = makeQuickJSError(bruFilePath, 10, 'data is not defined', 'ReferenceError');
525
+ const result = formatErrorWithContextV2(error, 'post-response', null, testDir);
526
+
527
+ expect(result).not.toBeNull();
528
+ expect(result.errorLine).toBe(1);
529
+ expect(result.errorType).toBe('ReferenceError');
530
+ });
531
+ });
532
+
533
+ describe('.yml file errors', () => {
534
+ it('should produce correct block-relative line numbers for pre-request (NodeVM)', () => {
535
+ const error = makeCallSiteError(ymlFilePath, 3, 'token error');
536
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
537
+
538
+ expect(result).not.toBeNull();
539
+ expect(result.errorLine).toBe(1);
540
+ expect(result.filePath).toBe('test.yml');
541
+ });
542
+
543
+ it('should produce correct block-relative line numbers for post-response (NodeVM)', () => {
544
+ const error = makeCallSiteError(ymlFilePath, 3, 'data error');
545
+ const result = formatErrorWithContextV2(error, 'post-response', null, testDir);
546
+
547
+ expect(result).not.toBeNull();
548
+ expect(result.errorLine).toBe(1);
549
+ });
550
+
551
+ it('should return null when yml script block has empty code', () => {
552
+ const emptyYml = [
553
+ 'info:',
554
+ ' name: empty-test',
555
+ ' version: "1"',
556
+ 'runtime:',
557
+ ' scripts:',
558
+ ' - type: before-request',
559
+ ' code: ""'
560
+ ].join('\n');
561
+ const emptyYmlPath = path.join(testDir, 'empty.yml');
562
+ fs.writeFileSync(emptyYmlPath, emptyYml);
563
+
564
+ const error = makeCallSiteError(emptyYmlPath, 3, 'error');
565
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
566
+
567
+ expect(result).toBeNull();
568
+ });
569
+ });
570
+
571
+ describe('segment resolution (collection/folder scripts)', () => {
572
+ it('should resolve errors in collection script segments', () => {
573
+ const metadata = {
574
+ requestStartLine: 5,
575
+ requestEndLine: 8,
576
+ segments: [{
577
+ startLine: 1,
578
+ endLine: 4,
579
+ filePath: collectionYmlPath
580
+ }]
581
+ };
582
+
583
+ const error = makeCallSiteError(bruFilePath, 4, 'x is not defined', 'ReferenceError');
584
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
585
+
586
+ expect(result).not.toBeNull();
587
+ expect(result.filePath).toBe('opencollection.yml');
588
+ expect(result.errorLine).toBe(1);
589
+ });
590
+
591
+ it('should return null when segment resolution fails', () => {
592
+ const metadata = {
593
+ requestStartLine: 5,
594
+ requestEndLine: 8,
595
+ segments: []
596
+ };
597
+
598
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
599
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
600
+
601
+ expect(result).toBeNull();
602
+ });
603
+ });
604
+
605
+ describe('stack trace extraction', () => {
606
+ it('should extract only "at" frames with indentation', () => {
607
+ const error = makeCallSiteError(bruFilePath, 3, 'test error', 'Error');
608
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
609
+
610
+ expect(result).not.toBeNull();
611
+ if (result.stack) {
612
+ const lines = result.stack.split('\n');
613
+ lines.forEach((line) => {
614
+ expect(line).toMatch(/^\s+at /);
615
+ });
616
+ }
617
+ });
618
+
619
+ it('should return stack as null when error has no stack', () => {
620
+ const error = makeCallSiteError(bruFilePath, 3, 'test error');
621
+ delete error.stack;
622
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
623
+
624
+ expect(result).not.toBeNull();
625
+ expect(result.stack).toBeNull();
626
+ });
627
+
628
+ it('should return stack as null when stack has no "at" frames', () => {
629
+ const error = makeCallSiteError(bruFilePath, 3, 'test error');
630
+ error.stack = 'Error: test error\nsome other info';
631
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
632
+
633
+ expect(result).not.toBeNull();
634
+ expect(result.stack).toBeNull();
635
+ });
636
+ });
637
+
638
+ describe('context lines filtering', () => {
639
+ it('should filter lines to block boundaries', () => {
640
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
641
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
642
+
643
+ expect(result).not.toBeNull();
644
+ result.lines.forEach((l) => {
645
+ expect(l.lineNumber).toBeGreaterThanOrEqual(1);
646
+ });
647
+ });
648
+
649
+ it('should mark the error line correctly', () => {
650
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
651
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
652
+
653
+ expect(result).not.toBeNull();
654
+ const errorLines = result.lines.filter((l) => l.isError);
655
+ expect(errorLines.length).toBe(1);
656
+ expect(errorLines[0].lineNumber).toBe(result.errorLine);
657
+ });
658
+
659
+ it('should return null when all context lines are filtered out', () => {
660
+ const emptyBru = `meta {
661
+ name: empty-test
662
+ type: http
663
+ seq: 1
664
+ }
665
+
666
+ script:pre-request {
667
+ }`;
668
+ const emptyBruPath = path.join(testDir, 'empty.bru');
669
+ fs.writeFileSync(emptyBruPath, emptyBru);
670
+
671
+ const error = makeCallSiteError(emptyBruPath, 3, 'error');
672
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
673
+
674
+ expect(result).toBeNull();
675
+ });
676
+
677
+ it('should return null when .bru file has no script block at all', () => {
678
+ const noScriptBru = `meta {
679
+ name: no-script-test
680
+ type: http
681
+ seq: 1
682
+ }
683
+
684
+ get {
685
+ url: https://example.com
686
+ }`;
687
+ const noScriptBruPath = path.join(testDir, 'no-script.bru');
688
+ fs.writeFileSync(noScriptBruPath, noScriptBru);
689
+
690
+ const error = makeCallSiteError(noScriptBruPath, 3, 'error');
691
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
692
+
693
+ expect(result).toBeNull();
694
+ });
695
+
696
+ it('should return null when .yml file has no script section at all', () => {
697
+ const noScriptYml = [
698
+ 'info:',
699
+ ' name: no-script-test',
700
+ ' version: "1"',
701
+ 'runtime:',
702
+ ' settings:',
703
+ ' timeout: 5000'
704
+ ].join('\n');
705
+ const noScriptYmlPath = path.join(testDir, 'no-script.yml');
706
+ fs.writeFileSync(noScriptYmlPath, noScriptYml);
707
+
708
+ const error = makeCallSiteError(noScriptYmlPath, 3, 'error');
709
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
710
+
711
+ expect(result).toBeNull();
712
+ });
713
+ });
714
+
715
+ describe('external JS file errors (no block offset)', () => {
716
+ it('should use absolute line numbers for non-.bru/.yml files', () => {
717
+ const jsFilePath = path.join(testDir, 'helper.js');
718
+ fs.writeFileSync(jsFilePath, 'function foo() {\n throw new Error("oops");\n}\nfoo();\n');
719
+
720
+ const error = makeCallSiteError(jsFilePath, 2, 'oops');
721
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
722
+
723
+ expect(result).not.toBeNull();
724
+ expect(result.errorLine).toBe(2);
725
+ expect(result.filePath).toBe('helper.js');
726
+ });
727
+ });
728
+
729
+ describe('scriptMetadata precedence', () => {
730
+ it('should prefer error.scriptMetadata over parameter when non-empty', () => {
731
+ const errorMetadata = {
732
+ requestStartLine: 1,
733
+ requestEndLine: 2
734
+ };
735
+ const paramMetadata = {
736
+ requestStartLine: 10,
737
+ requestEndLine: 20
738
+ };
739
+
740
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
741
+ error.scriptMetadata = errorMetadata;
742
+ const result = formatErrorWithContextV2(error, 'pre-request', paramMetadata, testDir);
743
+
744
+ expect(result).not.toBeNull();
745
+ });
746
+
747
+ it('should fall back to parameter when error.scriptMetadata is empty object', () => {
748
+ const paramMetadata = {
749
+ requestStartLine: 1,
750
+ requestEndLine: 2
751
+ };
752
+
753
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
754
+ error.scriptMetadata = {};
755
+ const result = formatErrorWithContextV2(error, 'pre-request', paramMetadata, testDir);
756
+
757
+ expect(result).not.toBeNull();
758
+ });
759
+
760
+ it('should fall back to parameter when error.scriptMetadata is null', () => {
761
+ const paramMetadata = {
762
+ requestStartLine: 1,
763
+ requestEndLine: 2
764
+ };
765
+
766
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
767
+ error.scriptMetadata = null;
768
+ const result = formatErrorWithContextV2(error, 'pre-request', paramMetadata, testDir);
769
+
770
+ expect(result).not.toBeNull();
771
+ });
772
+ });
773
+
774
+ describe('error type extraction', () => {
775
+ it('should extract error type from error.name', () => {
776
+ const error = makeCallSiteError(bruFilePath, 3, 'x is not defined', 'ReferenceError');
777
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
778
+
779
+ expect(result).not.toBeNull();
780
+ expect(result.errorType).toBe('ReferenceError');
781
+ });
782
+
783
+ it('should extract error type from error.cause.name', () => {
784
+ const error = makeCallSiteError(bruFilePath, 3, 'wrapped error');
785
+ error.cause = { name: 'TypeError', message: 'original error' };
786
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
787
+
788
+ expect(result).not.toBeNull();
789
+ expect(result.errorType).toBe('TypeError');
790
+ });
791
+ });
792
+
793
+ describe('display path', () => {
794
+ it('should compute relative path from collectionPath', () => {
795
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
796
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
797
+
798
+ expect(result).not.toBeNull();
799
+ expect(result.filePath).toBe('test.bru');
800
+ });
801
+
802
+ it('should fall back to absolute filePath when collectionPath is undefined', () => {
803
+ const error = makeCallSiteError(bruFilePath, 3, 'error');
804
+ const result = formatErrorWithContextV2(error, 'pre-request');
805
+
806
+ expect(result).not.toBeNull();
807
+ expect(result.filePath).toBe(bruFilePath);
808
+ });
809
+ });
810
+
811
+ describe('catch block logging', () => {
812
+ it('should log a warning when an internal error occurs', () => {
813
+ const error = new Error('test');
814
+ error.__callSites = [{ filePath: bruFilePath, line: 3, column: 1 }];
815
+ Object.defineProperty(error, 'name', {
816
+ get() { throw new Error('property access bomb'); }
817
+ });
818
+
819
+ const result = formatErrorWithContextV2(error, 'pre-request', null, testDir);
820
+ expect(result).toBeNull();
821
+ expect(consoleSpy).toHaveBeenCalled();
822
+ });
823
+ });
824
+
825
+ describe('in-memory script content (draft state)', () => {
826
+ it('should use requestScriptContent for request-level errors instead of disk', () => {
827
+ // The .bru file on disk has different content than the draft
828
+ const draftContent = 'const draft = true;\ndraft.missing.prop;\nconsole.log("draft");';
829
+ const metadata = {
830
+ requestStartLine: 1,
831
+ requestEndLine: 3,
832
+ requestScriptContent: draftContent
833
+ };
834
+
835
+ // NodeVM: line 4 - offset 2 = scriptRelativeLine 2, within request range [1,3]
836
+ // lineInScript = 2 - 1 = 1, which maps to first line of draft content
837
+ const error = makeCallSiteError(bruFilePath, 4, 'draft.missing is not defined', 'ReferenceError');
838
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
839
+
840
+ expect(result).not.toBeNull();
841
+ expect(result.errorLine).toBe(1);
842
+ expect(result.lines[0].content).toBe('const draft = true;');
843
+ expect(result.lines[0].isError).toBe(true);
844
+ });
845
+
846
+ it('should show correct error line from draft content', () => {
847
+ const draftContent = 'const a = 1;\nconst b = undefined;\nb.foo();';
848
+ const metadata = {
849
+ requestStartLine: 1,
850
+ requestEndLine: 3,
851
+ requestScriptContent: draftContent
852
+ };
853
+
854
+ // NodeVM: line 5 - offset 2 = scriptRelativeLine 3, within request range [1,3]
855
+ // lineInScript = 3 - 1 = 2, which maps to second line of draft content
856
+ const error = makeCallSiteError(bruFilePath, 5, 'b is undefined', 'TypeError');
857
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
858
+
859
+ expect(result).not.toBeNull();
860
+ expect(result.errorLine).toBe(2);
861
+ const errorLine = result.lines.find((l) => l.isError);
862
+ expect(errorLine.content).toBe('const b = undefined;');
863
+ });
864
+
865
+ it('should use scriptContent from segments for collection/folder errors', () => {
866
+ const draftCollectionScript = 'const draftCol = true;\ndraftCol.missing.prop;';
867
+ const metadata = {
868
+ requestStartLine: 0,
869
+ requestEndLine: 0,
870
+ segments: [{
871
+ startLine: 1,
872
+ endLine: 4,
873
+ filePath: collectionYmlPath,
874
+ displayPath: 'opencollection.yml',
875
+ scriptContent: draftCollectionScript
876
+ }]
877
+ };
878
+
879
+ // NodeVM: line 4 - offset 2 = scriptRelativeLine 2, falls in segment [1,4]
880
+ // lineInScript = 2 - 1 = 1, maps to first line of draft collection script
881
+ const error = makeCallSiteError(bruFilePath, 4, 'draftCol.missing is not defined', 'ReferenceError');
882
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
883
+
884
+ expect(result).not.toBeNull();
885
+ expect(result.filePath).toBe('opencollection.yml');
886
+ expect(result.errorLine).toBe(1);
887
+ expect(result.lines[0].content).toBe('const draftCol = true;');
888
+ expect(result.lines[0].isError).toBe(true);
889
+ });
890
+
891
+ it('should fall back to disk when requestScriptContent is not provided', () => {
892
+ const metadata = {
893
+ requestStartLine: 1,
894
+ requestEndLine: 3
895
+ // no requestScriptContent
896
+ };
897
+
898
+ const error = makeCallSiteError(bruFilePath, 3, 'token is not defined', 'ReferenceError');
899
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
900
+
901
+ expect(result).not.toBeNull();
902
+ // Should still work via disk-based fallback
903
+ expect(result.filePath).toBe('test.bru');
904
+ });
905
+
906
+ it('should fall back to disk when segment scriptContent is not provided', () => {
907
+ const metadata = {
908
+ requestStartLine: 0,
909
+ requestEndLine: 0,
910
+ segments: [{
911
+ startLine: 1,
912
+ endLine: 4,
913
+ filePath: collectionYmlPath,
914
+ displayPath: 'opencollection.yml'
915
+ // no scriptContent
916
+ }]
917
+ };
918
+
919
+ const error = makeCallSiteError(bruFilePath, 4, 'error', 'ReferenceError');
920
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
921
+
922
+ expect(result).not.toBeNull();
923
+ expect(result.filePath).toBe('opencollection.yml');
924
+ });
925
+
926
+ it('should use scriptContent from segments when folder .bru has no script block on disk', () => {
927
+ // Create a folder.bru with NO script block (simulates a draft where the user added a new script)
928
+ const folderBruPath = path.join(testDir, 'folder.bru');
929
+ fs.writeFileSync(folderBruPath, 'meta {\n name: My Folder\n}\n');
930
+
931
+ const draftFolderScript = 'const x = undefined;\nx.boom();';
932
+ const metadata = {
933
+ requestStartLine: 0,
934
+ requestEndLine: 0,
935
+ segments: [{
936
+ startLine: 1,
937
+ endLine: 4,
938
+ filePath: folderBruPath,
939
+ displayPath: 'folder/folder.bru',
940
+ scriptContent: draftFolderScript
941
+ }]
942
+ };
943
+
944
+ // NodeVM: line 4 - offset 2 = scriptRelativeLine 2, falls in segment [1,4]
945
+ // lineInScript = 2 - 1 = 1
946
+ const error = makeCallSiteError(bruFilePath, 4, 'x is undefined', 'TypeError');
947
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
948
+
949
+ expect(result).not.toBeNull();
950
+ expect(result.filePath).toBe('folder.bru');
951
+ expect(result.errorLine).toBe(1);
952
+ expect(result.lines[0].content).toBe('const x = undefined;');
953
+ expect(result.lines[0].isError).toBe(true);
954
+ });
955
+
956
+ it('should use QuickJS offset correctly with requestScriptContent', () => {
957
+ const draftContent = 'const x = 1;\nundefined.boom();';
958
+ const metadata = {
959
+ requestStartLine: 1,
960
+ requestEndLine: 3,
961
+ requestScriptContent: draftContent
962
+ };
963
+
964
+ // QuickJS: line 11 - offset 9 = scriptRelativeLine 2, within request range [1,3]
965
+ // lineInScript = 2 - 1 = 1
966
+ const error = makeQuickJSError(bruFilePath, 11, 'not defined', 'ReferenceError');
967
+ const result = formatErrorWithContextV2(error, 'pre-request', metadata, testDir);
968
+
969
+ expect(result).not.toBeNull();
970
+ expect(result.errorLine).toBe(1);
971
+ expect(result.lines[0].content).toBe('const x = 1;');
972
+ });
973
+ });
974
+ });
975
+
976
+ describe('stack trace with null-line segment resolution', () => {
977
+ it('buildStackFromCallSites should not interpolate null into stack frames', () => {
978
+ // Segment with no on-disk script block → resolveSegmentError returns line: null
979
+ const folderBruPath = path.join(testDir, 'folder.bru');
980
+ fs.writeFileSync(folderBruPath, 'meta {\n name: My Folder\n}\n');
981
+
982
+ const metadata = {
983
+ requestStartLine: 0,
984
+ requestEndLine: 0,
985
+ segments: [{
986
+ startLine: 1,
987
+ endLine: 4,
988
+ filePath: folderBruPath,
989
+ displayPath: 'folder/folder.bru',
990
+ scriptContent: 'const x = 1;\nx.boom();'
991
+ }]
992
+ };
993
+
994
+ // NodeVM callSite: line 4 - offset 2 = scriptRelativeLine 2, falls in segment [1,4]
995
+ const callSites = [{ filePath: bruFilePath, line: 4, column: 5, functionName: null }];
996
+ const result = buildStackFromCallSites(callSites, 'pre-request', null, metadata);
997
+
998
+ expect(result).not.toContain('null');
999
+ // Should fall back to original file/line since resolved.line is null
1000
+ expect(result).toContain(bruFilePath);
1001
+ });
1002
+
1003
+ it('adjustStackTrace should not interpolate null into stack frames', () => {
1004
+ const folderBruPath = path.join(testDir, 'folder.bru');
1005
+ fs.writeFileSync(folderBruPath, 'meta {\n name: My Folder\n}\n');
1006
+
1007
+ const metadata = {
1008
+ requestStartLine: 0,
1009
+ requestEndLine: 0,
1010
+ segments: [{
1011
+ startLine: 1,
1012
+ endLine: 4,
1013
+ filePath: folderBruPath,
1014
+ displayPath: 'folder/folder.bru',
1015
+ scriptContent: 'const x = 1;\nx.boom();'
1016
+ }]
1017
+ };
1018
+
1019
+ const stack = `TypeError: x.boom is not a function\n at ${bruFilePath}:4:5`;
1020
+ const result = adjustStackTrace(stack, 'pre-request', null, metadata, false);
1021
+
1022
+ expect(result).not.toContain('null');
1023
+ // Original frame should be preserved since resolved.line is null
1024
+ expect(result).toContain(`${bruFilePath}:4:5`);
1025
+ });
1026
+ });
1027
+
1028
+ describe('getSourceContextFromContent', () => {
1029
+ it('should return context lines around the error line', () => {
1030
+ const content = 'line1\nline2\nline3\nline4\nline5';
1031
+ const result = getSourceContextFromContent(content, 3, 1);
1032
+
1033
+ expect(result).not.toBeNull();
1034
+ expect(result.errorLine).toBe(3);
1035
+ expect(result.lines).toHaveLength(3);
1036
+ expect(result.lines[0]).toEqual({ lineNumber: 2, content: 'line2', isError: false });
1037
+ expect(result.lines[1]).toEqual({ lineNumber: 3, content: 'line3', isError: true });
1038
+ expect(result.lines[2]).toEqual({ lineNumber: 4, content: 'line4', isError: false });
1039
+ });
1040
+
1041
+ it('should return null for null/empty content', () => {
1042
+ expect(getSourceContextFromContent(null, 1)).toBeNull();
1043
+ expect(getSourceContextFromContent('', 1)).toBeNull();
1044
+ });
1045
+
1046
+ it('should return null for out-of-bounds error line', () => {
1047
+ const content = 'line1\nline2';
1048
+ expect(getSourceContextFromContent(content, 0)).toBeNull();
1049
+ expect(getSourceContextFromContent(content, 3)).toBeNull();
1050
+ });
1051
+
1052
+ it('should handle single-line content', () => {
1053
+ const content = 'only line';
1054
+ const result = getSourceContextFromContent(content, 1, 3);
1055
+
1056
+ expect(result).not.toBeNull();
1057
+ expect(result.lines).toHaveLength(1);
1058
+ expect(result.lines[0]).toEqual({ lineNumber: 1, content: 'only line', isError: true });
1059
+ });
1060
+
1061
+ it('should clamp context at file boundaries', () => {
1062
+ const content = 'line1\nline2\nline3';
1063
+ const result = getSourceContextFromContent(content, 1, 5);
1064
+
1065
+ expect(result).not.toBeNull();
1066
+ expect(result.lines).toHaveLength(3);
1067
+ expect(result.startLine).toBe(1);
1068
+ });
1069
+ });
388
1070
  });