diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index 92246ba80..a9f0fd343 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads' // Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads // These will be imported dynamically when needed -let event, container, Codecept, getConfig, tryOrDefault, deepMerge +let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack let stdout = '' @@ -21,10 +21,17 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData // Global error handlers to catch critical errors but not test failures process.on('uncaughtException', (err) => { + if (global.container?.tsFileMapping && fixErrorStack) { + const fileMapping = global.container.tsFileMapping() + if (fileMapping) { + fixErrorStack(err, fileMapping) + } + } + // Log to stderr to bypass stdout suppression process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`) process.stderr.write(`${err.stack}\n`) - + // Don't exit on test assertion errors - those are handled by mocha if (err.name === 'AssertionError' || err.message?.includes('expected')) { return @@ -33,13 +40,20 @@ process.on('uncaughtException', (err) => { }) process.on('unhandledRejection', (reason, promise) => { + if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) { + const fileMapping = global.container.tsFileMapping() + if (fileMapping) { + fixErrorStack(reason, fileMapping) + } + } + // Log to stderr to bypass stdout suppression const msg = reason?.message || String(reason) process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`) if (reason?.stack) { process.stderr.write(`${reason.stack}\n`) } - + // Don't exit on test-related rejections if (msg.includes('expected') || msg.includes('AssertionError')) { return @@ -132,13 +146,15 @@ initPromise = (async function () { const utilsModule = await import('../utils.js') const coreUtilsModule = await import('../../utils.js') const CodeceptModule = await import('../../codecept.js') - + const typescriptModule = await import('../../utils/typescript.js') + event = eventModule.default container = containerModule.default getConfig = utilsModule.getConfig tryOrDefault = coreUtilsModule.tryOrDefault deepMerge = coreUtilsModule.deepMerge Codecept = CodeceptModule.default + fixErrorStack = typescriptModule.fixErrorStack const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {}) @@ -147,6 +163,12 @@ initPromise = (async function () { // IMPORTANT: await is required here since getConfig is async baseConfig = await getConfig(options.config || testRoot) } catch (configErr) { + if (global.container?.tsFileMapping && fixErrorStack) { + const fileMapping = global.container.tsFileMapping() + if (fileMapping) { + fixErrorStack(configErr, fileMapping) + } + } process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`) process.stderr.write(`${configErr.stack}\n`) await new Promise(resolve => setTimeout(resolve, 100)) @@ -163,6 +185,12 @@ initPromise = (async function () { try { await codecept.init(testRoot) } catch (initErr) { + if (global.container?.tsFileMapping && fixErrorStack) { + const fileMapping = global.container.tsFileMapping() + if (fileMapping) { + fixErrorStack(initErr, fileMapping) + } + } process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`) process.stderr.write(`${initErr.stack}\n`) process.exit(1) @@ -190,6 +218,12 @@ initPromise = (async function () { parentPort?.close() } } catch (err) { + if (global.container?.tsFileMapping && fixErrorStack) { + const fileMapping = global.container.tsFileMapping() + if (fileMapping) { + fixErrorStack(err, fileMapping) + } + } process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`) process.stderr.write(`${err.stack}\n`) process.exit(1) diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js index 3b280f047..d905643e7 100644 --- a/lib/mocha/cli.js +++ b/lib/mocha/cli.js @@ -9,6 +9,7 @@ import event from '../event.js' import AssertionFailedError from '../assert/error.js' import output from '../output.js' import test, { cloneTest } from './test.js' +import { fixErrorStack } from '../utils/typescript.js' // Get version from package.json to avoid circular dependency const __filename = fileURLToPath(import.meta.url) @@ -264,6 +265,11 @@ class Cli extends Base { } try { + const fileMapping = global.container?.tsFileMapping?.() + if (fileMapping) { + fixErrorStack(err, fileMapping) + } + let stack = err.stack stack = (stack || '').replace(originalMessage, '') stack = stack ? stack.split('\n') : [] diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js index bf734684e..c7f2d96de 100644 --- a/lib/mocha/factory.js +++ b/lib/mocha/factory.js @@ -7,6 +7,7 @@ import gherkinParser, { loadTranslations } from './gherkin.js' import output from '../output.js' import scenarioUiFunction from './ui.js' import { initMochaGlobals } from '../globals.js' +import { fixErrorStack } from '../utils/typescript.js' const __filename = fileURLToPath(import.meta.url) const __dirname = fsPath.dirname(__filename) @@ -34,6 +35,10 @@ class MochaFactory { // Handle ECONNREFUSED without dynamic import for now err = new Error('Connection refused: ' + err.toString()) } + const fileMapping = global.container?.tsFileMapping?.() + if (fileMapping) { + fixErrorStack(err, fileMapping) + } output.error(err) output.print(err.stack) process.exit(1)