From 034413ba2ade1325c82f2166162e2e94bff05766 Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:38:47 +0800 Subject: [PATCH 1/4] fix: skip file-less stack frames in extractSource Native frames from stack-utils parse without a `file` path. Calling normalizeFile(undefined) threw TypeError and hid the original test failure during error serialization. Fixes #3483 --- lib/serialize-error.js | 3 ++- test-tap/serialize-error.js | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test-tap/serialize-error.js diff --git a/lib/serialize-error.js b/lib/serialize-error.js index ca47a6350..d437d9d62 100644 --- a/lib/serialize-error.js +++ b/lib/serialize-error.js @@ -26,7 +26,8 @@ function extractSource(stack, testFile) { for (const line of stack.split('\n')) { const callSite = stackUtils.parseLine(line); - if (callSite && normalizeFile(callSite.file) === testFile) { + // Native / eval frames can parse without a file path; skip them. + if (callSite?.file && normalizeFile(callSite.file) === testFile) { return { isDependency: false, isWithinProject: true, diff --git a/test-tap/serialize-error.js b/test-tap/serialize-error.js new file mode 100644 index 000000000..831525a59 --- /dev/null +++ b/test-tap/serialize-error.js @@ -0,0 +1,46 @@ +import path from 'node:path'; +import {pathToFileURL} from 'node:url'; + +import {test} from 'tap'; + +import serializeError from '../lib/serialize-error.js'; + +test('extractSource skips stack frames without a file path', t => { + const testFilePath = path.resolve('test-fixture.js'); + const testFile = pathToFileURL(testFilePath).toString(); + const error = new Error('boom'); + // Stack-utils leaves `file` undefined for native frames; previously + // normalizeFile(undefined) threw and hid the original test failure. + error.stack = `Error: boom + at Function.foo (native) + at Object. (${testFilePath}:10:5)`; + + const result = serializeError(error, {testFile}); + t.equal(result.type, 'native'); + t.same(result.source, { + isDependency: false, + isWithinProject: true, + file: testFile, + line: 10, + }); + t.end(); +}); + +test('extractSource skips anonymous file paths that are not the test file', t => { + const testFilePath = path.resolve('test-fixture.js'); + const testFile = pathToFileURL(testFilePath).toString(); + const error = new Error('boom'); + error.stack = `Error: boom + at Object. (:1:2) + at Object. (${testFilePath}:4:1)`; + + const result = serializeError(error, {testFile}); + t.equal(result.type, 'native'); + t.same(result.source, { + isDependency: false, + isWithinProject: true, + file: testFile, + line: 4, + }); + t.end(); +}); From 3cf261f4c2ced25cafbc7b14aa9a1d5b33106ed7 Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:07:30 -0700 Subject: [PATCH 2/4] test: normalize Node.js internal stack locations --- test-tap/helper/report.js | 1 + test-tap/reporters/tap.edgecases.v22.log | 2 +- test-tap/reporters/tap.edgecases.v24.log | 2 +- test-tap/reporters/tap.edgecases.v26.log | 2 +- test-tap/reporters/tap.js | 13 +++++++++++++ 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/test-tap/helper/report.js b/test-tap/helper/report.js index ac99df170..1f2dab3a5 100644 --- a/test-tap/helper/report.js +++ b/test-tap/helper/report.js @@ -46,6 +46,7 @@ exports.sanitizers = { experimentalWarning: string => string.replaceAll(/^\(node:\d+\) ExperimentalWarning.+\n/g, ''), lineEndings: string => string.replaceAll('\r\n', '\n'), libLineNumbers: string => string.replaceAll(/\((\/lib\/.+\.js):\d+:\d+\)/g, '($1)'), + nodeInternalLineNumbers: string => string.replaceAll(/\((node:internal\/[^)]+):\d+:\d+\)/g, '($1)'), // The following are injected by tap@18. posix: string => string.replaceAll('\\', '/'), tapLoaders: string => string.replaceAll(/.+(Module\._compile|node_modules.pirates|require\.extensions).+\r?\n/g, ''), diff --git a/test-tap/reporters/tap.edgecases.v22.log b/test-tap/reporters/tap.edgecases.v22.log index fc14a9e48..a92b27fa4 100644 --- a/test-tap/reporters/tap.edgecases.v22.log +++ b/test-tap/reporters/tap.edgecases.v22.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils:346:16)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.edgecases.v24.log b/test-tap/reporters/tap.edgecases.v24.log index 8e86fe155..a92b27fa4 100644 --- a/test-tap/reporters/tap.edgecases.v24.log +++ b/test-tap/reporters/tap.edgecases.v24.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils:318:16)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.edgecases.v26.log b/test-tap/reporters/tap.edgecases.v26.log index 8cd3b8fa8..a92b27fa4 100644 --- a/test-tap/reporters/tap.edgecases.v26.log +++ b/test-tap/reporters/tap.edgecases.v26.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils:354:16)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.js b/test-tap/reporters/tap.js index bac73e913..af8c194dc 100644 --- a/test-tap/reporters/tap.js +++ b/test-tap/reporters/tap.js @@ -24,6 +24,7 @@ test(async t => { report.sanitizers.esmLoader, report.sanitizers.experimentalWarning, report.sanitizers.libLineNumbers, + report.sanitizers.nodeInternalLineNumbers, report.sanitizers.posix, report.sanitizers.tapLoaders, report.sanitizers.timers, @@ -50,4 +51,16 @@ test(async t => { t.test('tap reporter - second failFast run', run('failFast2')); t.test('tap reporter - only run', run('only')); t.test('tap reporter - edge cases', run('edgeCases')); + t.test('Node.js internal stack locations are stable across runtime releases', t => { + for (const location of ['318:16', '319:16', '346:16', '355:16']) { + t.equal( + report.sanitizers.nodeInternalLineNumbers(`compileSourceTextModule (node:internal/modules/esm/utils:${location})`), + 'compileSourceTextModule (node:internal/modules/esm/utils)', + ); + } + + const applicationFrame = 'test (file:///project/test.js:3:1)'; + t.equal(report.sanitizers.nodeInternalLineNumbers(applicationFrame), applicationFrame); + t.end(); + }); }); From 7216266e939feab9944abd5b086af31082ad687b Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:39:57 +0800 Subject: [PATCH 3/4] test: drop unrelated reporter sanitizer from serialize-error PR Keep the PR focused on extractSource skipping file-less frames. Reporter line-number sanitizing belonged elsewhere and was called out in review. --- test-tap/helper/report.js | 1 - test-tap/reporters/tap.edgecases.v22.log | 2 +- test-tap/reporters/tap.edgecases.v24.log | 2 +- test-tap/reporters/tap.edgecases.v26.log | 2 +- test-tap/reporters/tap.js | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test-tap/helper/report.js b/test-tap/helper/report.js index 1f2dab3a5..ac99df170 100644 --- a/test-tap/helper/report.js +++ b/test-tap/helper/report.js @@ -46,7 +46,6 @@ exports.sanitizers = { experimentalWarning: string => string.replaceAll(/^\(node:\d+\) ExperimentalWarning.+\n/g, ''), lineEndings: string => string.replaceAll('\r\n', '\n'), libLineNumbers: string => string.replaceAll(/\((\/lib\/.+\.js):\d+:\d+\)/g, '($1)'), - nodeInternalLineNumbers: string => string.replaceAll(/\((node:internal\/[^)]+):\d+:\d+\)/g, '($1)'), // The following are injected by tap@18. posix: string => string.replaceAll('\\', '/'), tapLoaders: string => string.replaceAll(/.+(Module\._compile|node_modules.pirates|require\.extensions).+\r?\n/g, ''), diff --git a/test-tap/reporters/tap.edgecases.v22.log b/test-tap/reporters/tap.edgecases.v22.log index a92b27fa4..fc14a9e48 100644 --- a/test-tap/reporters/tap.edgecases.v22.log +++ b/test-tap/reporters/tap.edgecases.v22.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils:346:16)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.edgecases.v24.log b/test-tap/reporters/tap.edgecases.v24.log index a92b27fa4..8db3e4442 100644 --- a/test-tap/reporters/tap.edgecases.v24.log +++ b/test-tap/reporters/tap.edgecases.v24.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils:319:16)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.edgecases.v26.log b/test-tap/reporters/tap.edgecases.v26.log index a92b27fa4..9099e9600 100644 --- a/test-tap/reporters/tap.edgecases.v26.log +++ b/test-tap/reporters/tap.edgecases.v26.log @@ -4,7 +4,7 @@ not ok 1 - SyntaxError: Unexpected token 'do' --- name: SyntaxError message: Unexpected token 'do' - at: 'compileSourceTextModule (node:internal/modules/esm/utils)' + at: 'compileSourceTextModule (node:internal/modules/esm/utils:355:16)' ... ---tty-stream-chunk-separator not ok 2 - ast-syntax-error.js exited with a non-zero exit code: 1 diff --git a/test-tap/reporters/tap.js b/test-tap/reporters/tap.js index d02fa0455..bac73e913 100644 --- a/test-tap/reporters/tap.js +++ b/test-tap/reporters/tap.js @@ -24,7 +24,6 @@ test(async t => { report.sanitizers.esmLoader, report.sanitizers.experimentalWarning, report.sanitizers.libLineNumbers, - report.sanitizers.nodeInternalLineNumbers, report.sanitizers.posix, report.sanitizers.tapLoaders, report.sanitizers.timers, From 17a54221cb6d398e177b8de58a2368b8dbfd6969 Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:57:41 +0800 Subject: [PATCH 4/4] test: allow synthetic error stacks in serialization fixtures --- test/serialize-error.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/serialize-error.js b/test/serialize-error.js index 698ac653b..6eceb4a3b 100644 --- a/test/serialize-error.js +++ b/test/serialize-error.js @@ -11,6 +11,7 @@ test('extractSource skips stack frames without a file path', t => { const error = new Error('boom'); // Stack-utils leaves `file` undefined for native frames; previously // normalizeFile(undefined) threw and hid the original test failure. + // eslint-disable-next-line unicorn/no-error-property-assignment -- Deliberately construct a native stack frame for this regression test. error.stack = `Error: boom at Function.foo (native) at Object. (${testFilePath}:10:5)`; @@ -29,6 +30,7 @@ test('extractSource skips anonymous file paths that are not the test file', t => const testFilePath = path.resolve('test-fixture.js'); const testFile = pathToFileURL(testFilePath).toString(); const error = new Error('boom'); + // eslint-disable-next-line unicorn/no-error-property-assignment -- Deliberately construct an anonymous stack frame for this regression test. error.stack = `Error: boom at Object. (:1:2) at Object. (${testFilePath}:4:1)`;