@usebruno/js 0.45.1 → 0.46.1
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/package.json +5 -4
- package/src/bru.js +110 -18
- package/src/bruno-request.js +21 -19
- package/src/index.js +4 -1
- package/src/runtime/script-runtime.js +107 -62
- package/src/runtime/test-runtime.js +8 -3
- package/src/sandbox/node-vm/console.js +102 -0
- package/src/sandbox/node-vm/index.js +68 -8
- package/src/sandbox/node-vm/utils.js +15 -0
- package/src/sandbox/quickjs/index.js +6 -21
- package/src/sandbox/quickjs/shims/bru.js +108 -3
- package/src/sandbox/quickjs/shims/bruno-request.js +12 -0
- package/src/sandbox/quickjs/shims/console.js +97 -5
- package/src/sandbox/quickjs/shims/lib/axios.js +26 -16
- package/src/sandbox/quickjs/shims/lib/axios.spec.js +495 -0
- package/src/test.js +6 -4
- package/src/utils/error-formatter.js +426 -0
- package/src/utils/error-formatter.spec.js +388 -0
- package/src/utils/sandbox.js +64 -0
- package/src/utils.js +27 -6
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
|
|
2
|
+
const {
|
|
3
|
+
formatErrorWithContext,
|
|
4
|
+
findScriptBlockStartLine,
|
|
5
|
+
findYmlScriptBlockStartLine,
|
|
6
|
+
adjustLineNumber,
|
|
7
|
+
parseStackTrace,
|
|
8
|
+
parseErrorLocation
|
|
9
|
+
} = require('./error-formatter');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
// Line numbers annotated for reference:
|
|
15
|
+
// 13: script:pre-request { → blockStartLine = 14
|
|
16
|
+
// 14: const token = ... → script line 1
|
|
17
|
+
// 18: script:post-response { → blockStartLine = 19
|
|
18
|
+
// 19: const data = res.body; → script line 1
|
|
19
|
+
// 20: bru.setVar(...) → script line 2
|
|
20
|
+
// 24: tests { → blockStartLine = 25
|
|
21
|
+
// 25: test("status is 200"...) → script line 1
|
|
22
|
+
const MULTI_BLOCK_BRU = `meta {
|
|
23
|
+
name: multi-block-test
|
|
24
|
+
type: http
|
|
25
|
+
seq: 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get {
|
|
29
|
+
url: https://example.com
|
|
30
|
+
body: none
|
|
31
|
+
auth: none
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
script:pre-request {
|
|
35
|
+
const token = bru.getEnvVar('token');
|
|
36
|
+
req.setHeader('Authorization', token);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
script:post-response {
|
|
40
|
+
const data = res.body;
|
|
41
|
+
bru.setVar('userId', data.id);
|
|
42
|
+
console.log(data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tests {
|
|
46
|
+
test("status is 200", function() {
|
|
47
|
+
expect(res.status).to.equal(200);
|
|
48
|
+
});
|
|
49
|
+
test("has body", function() {
|
|
50
|
+
expect(res.body).to.not.be.null;
|
|
51
|
+
});
|
|
52
|
+
}`;
|
|
53
|
+
|
|
54
|
+
// Fixture with JS comments to verify line mapping when comments are present.
|
|
55
|
+
// 11: script:post-response { → blockStartLine = 12
|
|
56
|
+
// 12: // This is a comment → script line 1
|
|
57
|
+
// 13: const data = res.body; → script line 2
|
|
58
|
+
// 14: // Another comment → script line 3
|
|
59
|
+
// 15: bru.setVar('userId', ...); → script line 4
|
|
60
|
+
const BRU_WITH_COMMENTS = `meta {
|
|
61
|
+
name: comment-test
|
|
62
|
+
type: http
|
|
63
|
+
seq: 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get {
|
|
67
|
+
url: https://example.com
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
script:post-response {
|
|
71
|
+
// This is a comment
|
|
72
|
+
const data = res.body;
|
|
73
|
+
// Another comment
|
|
74
|
+
bru.setVar('userId', data.id);
|
|
75
|
+
}`;
|
|
76
|
+
|
|
77
|
+
// YML fixture: blockStartLine = 8 (pre-request), 12 (post-response), 16 (tests)
|
|
78
|
+
const MULTI_BLOCK_YML = [
|
|
79
|
+
'info:',
|
|
80
|
+
' name: yaml-test',
|
|
81
|
+
' version: "1"',
|
|
82
|
+
'runtime:',
|
|
83
|
+
' scripts:',
|
|
84
|
+
' - type: before-request',
|
|
85
|
+
' code: |-',
|
|
86
|
+
' const token = bru.getEnvVar(\'token\');',
|
|
87
|
+
' req.setHeader(\'Authorization\', token);',
|
|
88
|
+
' - type: after-response',
|
|
89
|
+
' code: |-',
|
|
90
|
+
' const data = res.body;',
|
|
91
|
+
' bru.setVar(\'userId\', data.id);',
|
|
92
|
+
' - type: tests',
|
|
93
|
+
' code: |-',
|
|
94
|
+
' test("status is 200", function() {',
|
|
95
|
+
' expect(res.status).to.equal(200);',
|
|
96
|
+
' });'
|
|
97
|
+
].join('\n');
|
|
98
|
+
|
|
99
|
+
// Collection/folder yml : scripts at request.scripts
|
|
100
|
+
// blockStartLine: before-request = 5, tests = 9
|
|
101
|
+
const COLLECTION_YML = [
|
|
102
|
+
'info:',
|
|
103
|
+
' name: test-collection',
|
|
104
|
+
'request:',
|
|
105
|
+
' scripts:',
|
|
106
|
+
' - type: before-request',
|
|
107
|
+
' code: |-',
|
|
108
|
+
' const abc = fc()',
|
|
109
|
+
' const x = bru.getVar(\'x\');',
|
|
110
|
+
' - type: tests',
|
|
111
|
+
' code: |-',
|
|
112
|
+
' test("example", function() {',
|
|
113
|
+
' expect(true).to.be.true;',
|
|
114
|
+
' });'
|
|
115
|
+
].join('\n');
|
|
116
|
+
|
|
117
|
+
// Wrapper offsets: QuickJS = 9 (script line 1 = VM line 10), NodeVM = 2 (script line 1 = VM line 3)
|
|
118
|
+
|
|
119
|
+
describe('Error Formatter', () => {
|
|
120
|
+
let testDir;
|
|
121
|
+
let bruFilePath;
|
|
122
|
+
let ymlFilePath;
|
|
123
|
+
let bruWithCommentsPath;
|
|
124
|
+
let collectionYmlPath;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));
|
|
128
|
+
bruFilePath = path.join(testDir, 'test.bru');
|
|
129
|
+
ymlFilePath = path.join(testDir, 'test.yml');
|
|
130
|
+
bruWithCommentsPath = path.join(testDir, 'comments.bru');
|
|
131
|
+
collectionYmlPath = path.join(testDir, 'opencollection.yml');
|
|
132
|
+
fs.writeFileSync(bruFilePath, MULTI_BLOCK_BRU);
|
|
133
|
+
fs.writeFileSync(ymlFilePath, MULTI_BLOCK_YML);
|
|
134
|
+
fs.writeFileSync(bruWithCommentsPath, BRU_WITH_COMMENTS);
|
|
135
|
+
fs.writeFileSync(collectionYmlPath, COLLECTION_YML);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('findScriptBlockStartLine', () => {
|
|
143
|
+
it('should find each block type in .bru files', () => {
|
|
144
|
+
expect(findScriptBlockStartLine(bruFilePath, 'pre-request')).toBe(14);
|
|
145
|
+
expect(findScriptBlockStartLine(bruFilePath, 'post-response')).toBe(19);
|
|
146
|
+
expect(findScriptBlockStartLine(bruFilePath, 'test')).toBe(25);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should return null for missing block or non-.bru files', () => {
|
|
150
|
+
const noBlockPath = path.join(testDir, 'no-block.bru');
|
|
151
|
+
fs.writeFileSync(noBlockPath, 'meta {\n name: test\n}');
|
|
152
|
+
expect(findScriptBlockStartLine(noBlockPath, 'post-response')).toBeNull();
|
|
153
|
+
expect(findScriptBlockStartLine('/some/file.js', 'post-response')).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('findYmlScriptBlockStartLine', () => {
|
|
158
|
+
it('should find each block type in .yml files', () => {
|
|
159
|
+
expect(findYmlScriptBlockStartLine(ymlFilePath, 'pre-request')).toBe(8);
|
|
160
|
+
expect(findYmlScriptBlockStartLine(ymlFilePath, 'post-response')).toBe(12);
|
|
161
|
+
expect(findYmlScriptBlockStartLine(ymlFilePath, 'test')).toBe(16);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should find script blocks in collection/folder yml files (request.scripts path)', () => {
|
|
165
|
+
expect(findYmlScriptBlockStartLine(collectionYmlPath, 'pre-request')).toBe(7);
|
|
166
|
+
expect(findYmlScriptBlockStartLine(collectionYmlPath, 'test')).toBe(11);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return null for missing block or non-.yml files', () => {
|
|
170
|
+
const noRuntimePath = path.join(testDir, 'no-runtime.yml');
|
|
171
|
+
fs.writeFileSync(noRuntimePath, 'info:\n name: simple\n version: "1"\n');
|
|
172
|
+
expect(findYmlScriptBlockStartLine(noRuntimePath, 'pre-request')).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('adjustLineNumber', () => {
|
|
177
|
+
it('should adjust QuickJS lines for .bru files', () => {
|
|
178
|
+
// VM line - offset(9) = scriptLine → blockStart + scriptLine - 1
|
|
179
|
+
expect(adjustLineNumber(bruFilePath, 10, true, 'pre-request')).toBe(14);
|
|
180
|
+
expect(adjustLineNumber(bruFilePath, 11, true, 'post-response')).toBe(20);
|
|
181
|
+
expect(adjustLineNumber(bruFilePath, 10, true, 'test')).toBe(25);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should adjust NodeVM lines for .bru files', () => {
|
|
185
|
+
// VM line 4 - offset(2) = scriptLine 2 → blockStart(19) + 2 - 1 = 20
|
|
186
|
+
expect(adjustLineNumber(bruFilePath, 4, false, 'post-response')).toBe(20);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should adjust lines for .yml files', () => {
|
|
190
|
+
expect(adjustLineNumber(ymlFilePath, 10, true, 'pre-request')).toBe(8);
|
|
191
|
+
expect(adjustLineNumber(ymlFilePath, 11, true, 'post-response')).toBe(13);
|
|
192
|
+
expect(adjustLineNumber(ymlFilePath, 4, false, 'post-response')).toBe(13);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should adjust lines correctly when script has comments', () => {
|
|
196
|
+
// VM line 12 - offset(9) = scriptLine 3 → blockStart(12) + 3 - 1 = 14
|
|
197
|
+
expect(adjustLineNumber(bruWithCommentsPath, 12, true, 'post-response')).toBe(14);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return reportedLine for non-.bru/.yml files or invalid offset', () => {
|
|
201
|
+
expect(adjustLineNumber('/some/file.js', 10, true, 'post-response')).toBe(10);
|
|
202
|
+
// VM line 5 - offset(9) = -4, which is < 1
|
|
203
|
+
expect(adjustLineNumber(bruFilePath, 5, true, 'post-response')).toBe(5);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should use metadata for combined scripts', () => {
|
|
207
|
+
// scriptLine 5 within request range [5, 7] → blockStart(19) + (5-5) - 1 = 18
|
|
208
|
+
const metadata = { requestStartLine: 5, requestEndLine: 7 };
|
|
209
|
+
expect(adjustLineNumber(bruFilePath, 14, true, 'post-response', null, metadata)).toBe(18);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return null for collection/folder segment errors', () => {
|
|
213
|
+
// scriptLine 3 is before requestStartLine(10) → cannot map to request file
|
|
214
|
+
const metadata = { requestStartLine: 10, requestEndLine: 15 };
|
|
215
|
+
expect(adjustLineNumber(bruFilePath, 12, true, 'post-response', null, metadata)).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should return null when request segment is empty', () => {
|
|
219
|
+
// requestStartLine: 0 indicates the request segment was empty
|
|
220
|
+
const metadata = { requestStartLine: 0, requestEndLine: 0 };
|
|
221
|
+
expect(adjustLineNumber(bruFilePath, 12, true, 'post-response', null, metadata)).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('parseStackTrace', () => {
|
|
226
|
+
it('should detect QuickJS stack frame formats', () => {
|
|
227
|
+
expect(parseStackTrace('Error: test\n at (/path/file.bru:11)'))
|
|
228
|
+
.toMatchObject({ filePath: '/path/file.bru', line: 11, isQuickJS: true });
|
|
229
|
+
expect(parseStackTrace('Error: test\n at <anonymous> (/path/file.bru:11)'))
|
|
230
|
+
.toMatchObject({ filePath: '/path/file.bru', line: 11, isQuickJS: true });
|
|
231
|
+
expect(parseStackTrace('Error: test\n at <eval> (/path/file.bru:11:5)'))
|
|
232
|
+
.toMatchObject({ filePath: '/path/file.bru', line: 11, column: 5, isQuickJS: true });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should detect NodeVM stack frame formats', () => {
|
|
236
|
+
expect(parseStackTrace('Error: test\n at /path/file.js:10:5'))
|
|
237
|
+
.toMatchObject({ filePath: '/path/file.js', line: 10, column: 5, isQuickJS: false });
|
|
238
|
+
expect(parseStackTrace('Error: test\n at Object.<anonymous> (/path/file.js:10:5)'))
|
|
239
|
+
.toMatchObject({ filePath: '/path/file.js', line: 10, column: 5, isQuickJS: false });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return null for unparseable or null input', () => {
|
|
243
|
+
expect(parseStackTrace('just a plain string')).toBeNull();
|
|
244
|
+
expect(parseStackTrace(null)).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('formatErrorWithContext', () => {
|
|
249
|
+
it('should format error with arrow pointing at the correct line', () => {
|
|
250
|
+
const error = new Error('data is not defined');
|
|
251
|
+
error.name = 'ReferenceError';
|
|
252
|
+
error.stack = `ReferenceError: data is not defined\n at (${bruFilePath}:10)`;
|
|
253
|
+
|
|
254
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');
|
|
255
|
+
expect(formatted).toContain('ReferenceError: data is not defined');
|
|
256
|
+
|
|
257
|
+
const arrowLine = formatted.split('\n').find((l) => l.startsWith('>'));
|
|
258
|
+
expect(arrowLine).toContain('const data = res.body;');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should show original error type for wrapped QuickJS errors', () => {
|
|
262
|
+
const error = new Error('x is not defined');
|
|
263
|
+
error.name = 'QuickJSUnwrapError';
|
|
264
|
+
error.cause = { name: 'ReferenceError', message: 'x is not defined' };
|
|
265
|
+
error.stack = `QuickJSUnwrapError: x is not defined\n at (${bruFilePath}:10)`;
|
|
266
|
+
|
|
267
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');
|
|
268
|
+
expect(formatted).toContain('ReferenceError:');
|
|
269
|
+
expect(formatted).not.toContain('QuickJSUnwrapError');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should use __callSites and adjust line numbers in stack', () => {
|
|
273
|
+
const error = new Error('data is not defined');
|
|
274
|
+
error.name = 'ReferenceError';
|
|
275
|
+
error.stack = `ReferenceError: data is not defined\n at ${bruFilePath}:4:5`;
|
|
276
|
+
error.__callSites = [{ filePath: bruFilePath, line: 4, column: 5, functionName: null }];
|
|
277
|
+
|
|
278
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');
|
|
279
|
+
// VM line 4 → file line 20
|
|
280
|
+
expect(formatted).toContain(`${bruFilePath}:20:5`);
|
|
281
|
+
|
|
282
|
+
const arrowLine = formatted.split('\n').find((l) => l.startsWith('>'));
|
|
283
|
+
expect(arrowLine).toContain('bru.setVar');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should show message-only output for collection/folder script errors', () => {
|
|
287
|
+
const error = new Error('x is not defined');
|
|
288
|
+
error.name = 'ReferenceError';
|
|
289
|
+
// scriptLine 3 (VM 12 - offset 9) is before requestStartLine(10)
|
|
290
|
+
error.stack = `ReferenceError: x is not defined\n at (${bruFilePath}:12)`;
|
|
291
|
+
|
|
292
|
+
const metadata = { requestStartLine: 10, requestEndLine: 15 };
|
|
293
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'post-response', 5, metadata);
|
|
294
|
+
|
|
295
|
+
expect(formatted).toContain('ReferenceError: x is not defined');
|
|
296
|
+
// Should NOT show source context from the request file
|
|
297
|
+
expect(formatted).not.toContain('File:');
|
|
298
|
+
expect(formatted).not.toContain('meta {');
|
|
299
|
+
expect(formatted).not.toContain('>');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should show source context from collection.bru when segments are provided', () => {
|
|
303
|
+
const collectionBruPath = path.join(testDir, 'collection.bru');
|
|
304
|
+
fs.writeFileSync(collectionBruPath, 'meta {\n name: My Collection\n}\n\nscript:pre-request {\n const x = undefined;\n x.foo();\n}');
|
|
305
|
+
|
|
306
|
+
const error = new Error('Cannot read properties of undefined');
|
|
307
|
+
error.name = 'TypeError';
|
|
308
|
+
// NodeVM offset=2, scriptRelativeLine = 5-2 = 3 → line 3 of wrapped segment = x.foo()
|
|
309
|
+
error.stack = `TypeError: Cannot read properties of undefined\n at ${bruFilePath}:5:5`;
|
|
310
|
+
|
|
311
|
+
// Collection segment is lines 1-4 in combined script (3-line wrap of 2-line script)
|
|
312
|
+
const metadata = {
|
|
313
|
+
requestStartLine: 0,
|
|
314
|
+
requestEndLine: 0,
|
|
315
|
+
segments: [
|
|
316
|
+
{ startLine: 1, endLine: 4, filePath: collectionBruPath, displayPath: 'collection.bru' }
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'pre-request', 5, metadata);
|
|
321
|
+
expect(formatted).toContain('File: collection.bru');
|
|
322
|
+
expect(formatted).toContain('x.foo()');
|
|
323
|
+
expect(formatted).toContain('TypeError: Cannot read properties of undefined');
|
|
324
|
+
const arrowLine = formatted.split('\n').find((l) => l.startsWith('>'));
|
|
325
|
+
expect(arrowLine).toContain('x.foo()');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should resolve error to correct folder when multiple segments exist', () => {
|
|
329
|
+
const folder1Dir = path.join(testDir, 'folder1');
|
|
330
|
+
const folder2Dir = path.join(testDir, 'folder2');
|
|
331
|
+
fs.mkdirSync(folder1Dir);
|
|
332
|
+
fs.mkdirSync(folder2Dir);
|
|
333
|
+
|
|
334
|
+
const folder1Bru = path.join(folder1Dir, 'folder.bru');
|
|
335
|
+
const folder2Bru = path.join(folder2Dir, 'folder.bru');
|
|
336
|
+
fs.writeFileSync(folder1Bru, 'meta {\n name: Folder1\n}\n\nscript:pre-request {\n let a = 1;\n}');
|
|
337
|
+
fs.writeFileSync(folder2Bru, 'meta {\n name: Folder2\n}\n\nscript:pre-request {\n let b = undefined;\n b.pop();\n}');
|
|
338
|
+
|
|
339
|
+
const error = new Error('Cannot read properties of undefined');
|
|
340
|
+
error.name = 'TypeError';
|
|
341
|
+
// NodeVM offset=2, scriptRelativeLine = 9-2 = 7, falls in folder2 segment [5,7]
|
|
342
|
+
error.stack = `TypeError: Cannot read properties of undefined\n at ${bruFilePath}:9:5`;
|
|
343
|
+
|
|
344
|
+
const metadata = {
|
|
345
|
+
requestStartLine: 0,
|
|
346
|
+
requestEndLine: 0,
|
|
347
|
+
segments: [
|
|
348
|
+
{ startLine: 1, endLine: 3, filePath: folder1Bru, displayPath: 'folder1/folder.bru' },
|
|
349
|
+
{ startLine: 5, endLine: 7, filePath: folder2Bru, displayPath: 'folder2/folder.bru' }
|
|
350
|
+
]
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const formatted = formatErrorWithContext(error, 'test.bru', 'pre-request', 5, metadata);
|
|
354
|
+
expect(formatted).toContain('File: folder2/folder.bru');
|
|
355
|
+
expect(formatted).toContain('b.pop()');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should resolve collection yml segment errors to opencollection.yml', () => {
|
|
359
|
+
const error = new Error('\'fc\' is not defined');
|
|
360
|
+
error.name = 'ReferenceError';
|
|
361
|
+
error.__isQuickJS = true;
|
|
362
|
+
// QuickJS offset=9, scriptRelativeLine = 11-9 = 2 → falls in collection segment [1,4]
|
|
363
|
+
error.stack = `ReferenceError: 'fc' is not defined\n at <anonymous> (${ymlFilePath}:11)`;
|
|
364
|
+
|
|
365
|
+
const metadata = {
|
|
366
|
+
requestStartLine: 6,
|
|
367
|
+
requestEndLine: 8,
|
|
368
|
+
segments: [
|
|
369
|
+
{ startLine: 1, endLine: 4, filePath: collectionYmlPath, displayPath: 'opencollection.yml' }
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const formatted = formatErrorWithContext(error, 'test.yml', 'pre-request', 5, metadata);
|
|
374
|
+
expect(formatted).toContain('File: opencollection.yml');
|
|
375
|
+
expect(formatted).toContain('\'fc\' is not defined');
|
|
376
|
+
const arrowLine = formatted.split('\n').find((l) => l.startsWith('>'));
|
|
377
|
+
expect(arrowLine).toContain('fc()');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should handle edge cases gracefully', () => {
|
|
381
|
+
expect(formatErrorWithContext(null)).toBe('');
|
|
382
|
+
|
|
383
|
+
const error = new Error('Test error');
|
|
384
|
+
error.stack = 'Invalid stack trace';
|
|
385
|
+
expect(formatErrorWithContext(error)).toContain('Test error');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Sandbox script wrapping utilities for Node VM and QuickJS.
|
|
2
|
+
// Line offsets are computed from the prefix strings so error-formatter.js can map
|
|
3
|
+
// VM-reported line numbers back to the original .bru/.yml source lines.
|
|
4
|
+
|
|
5
|
+
const SANDBOX = Object.freeze({
|
|
6
|
+
NODEVM: 'nodevm',
|
|
7
|
+
QUICKJS: 'quickjs'
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// -- Node VM --
|
|
11
|
+
|
|
12
|
+
const NODEVM_SCRIPT_PREFIX = `
|
|
13
|
+
(async function(){
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const NODEVM_SCRIPT_SUFFIX = `
|
|
17
|
+
})();
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
// -- QuickJS --
|
|
21
|
+
|
|
22
|
+
const QUICKJS_SCRIPT_PREFIX = `
|
|
23
|
+
(async () => {
|
|
24
|
+
const setTimeout = async(fn, timer) => {
|
|
25
|
+
v = await bru.sleep(timer);
|
|
26
|
+
fn.apply();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await bru.sleep(0);
|
|
30
|
+
try {
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const QUICKJS_SCRIPT_SUFFIX = `
|
|
34
|
+
}
|
|
35
|
+
catch(error) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return 'done';
|
|
39
|
+
})()
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
// Computed offsets — number of newlines before user script in each wrapper
|
|
43
|
+
const NODEVM_SCRIPT_WRAPPER_OFFSET = NODEVM_SCRIPT_PREFIX.split('\n').length - 1;
|
|
44
|
+
const QUICKJS_SCRIPT_WRAPPER_OFFSET = QUICKJS_SCRIPT_PREFIX.split('\n').length - 1;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wraps a script in the appropriate sandbox closure.
|
|
48
|
+
* @param {string} script - The script code to wrap
|
|
49
|
+
* @param {'nodevm'|'quickjs'} sandbox - The sandbox runtime to wrap for
|
|
50
|
+
* @returns {string} The wrapped script
|
|
51
|
+
*/
|
|
52
|
+
const wrapScriptInClosure = (script, sandbox) => {
|
|
53
|
+
if (sandbox === SANDBOX.QUICKJS) {
|
|
54
|
+
return QUICKJS_SCRIPT_PREFIX + script + QUICKJS_SCRIPT_SUFFIX;
|
|
55
|
+
}
|
|
56
|
+
return NODEVM_SCRIPT_PREFIX + script + NODEVM_SCRIPT_SUFFIX;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
SANDBOX,
|
|
61
|
+
wrapScriptInClosure,
|
|
62
|
+
NODEVM_SCRIPT_WRAPPER_OFFSET,
|
|
63
|
+
QUICKJS_SCRIPT_WRAPPER_OFFSET
|
|
64
|
+
};
|
package/src/utils.js
CHANGED
|
@@ -162,13 +162,34 @@ const cleanJson = (data) => {
|
|
|
162
162
|
].filter(Boolean);
|
|
163
163
|
const binaryNames = typedArrays.map((d) => d.name);
|
|
164
164
|
|
|
165
|
+
const seen = new WeakSet();
|
|
166
|
+
|
|
165
167
|
const replacer = (key, value) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
if (typeof value === 'object' && value !== null) {
|
|
169
|
+
if (seen.has(value)) {
|
|
170
|
+
return '[Circular Reference]';
|
|
171
|
+
}
|
|
172
|
+
seen.add(value);
|
|
173
|
+
|
|
174
|
+
// instanceof + [[Class]] cover same-realm; duck-type fallback for cross-realm/cross-context Error-like objects
|
|
175
|
+
if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {
|
|
176
|
+
const error = {};
|
|
177
|
+
// name/message are often on prototype; ensure they're in the output
|
|
178
|
+
error.name = value.name;
|
|
179
|
+
error.message = value.message;
|
|
180
|
+
Object.getOwnPropertyNames(value).forEach((prop) => {
|
|
181
|
+
error[prop] = value[prop];
|
|
182
|
+
});
|
|
183
|
+
return error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const isBinary = typedArrays.find((d) => value instanceof d);
|
|
187
|
+
if (isBinary) {
|
|
188
|
+
return {
|
|
189
|
+
__cleanJSONType: isBinary.name,
|
|
190
|
+
__cleanJSONValue: Buffer.from(value.buffer).toJSON()
|
|
191
|
+
};
|
|
192
|
+
}
|
|
172
193
|
}
|
|
173
194
|
return value;
|
|
174
195
|
};
|