From 9ea296657e23a4483ee6304f7175d5491897ea93 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Wed, 17 Jun 2026 14:06:18 +0200 Subject: [PATCH] fix(typescript): unique temp file names to fix run-multiple race (#5642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transpiled TypeScript files were written to a fixed ".temp.mjs" path next to the source. Under run-multiple, every forked worker transpiles the same files to the same temp paths and cleans them up independently, so one worker's cleanup deletes files the others still need to import — surfacing as "Cannot find module *.temp.mjs". Include process.pid plus a random suffix in the temp file name so each worker writes (and removes) its own files. The names still end in ".temp.mjs", so stack-trace remapping and fixErrorStack keep working. Co-Authored-By: Claude Opus 4.8 --- lib/utils/typescript.js | 4 +++- test/unit/utils/typescript_test.js | 36 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test/unit/utils/typescript_test.js diff --git a/lib/utils/typescript.js b/lib/utils/typescript.js index 5f9d0d417..3ee0fda7b 100644 --- a/lib/utils/typescript.js +++ b/lib/utils/typescript.js @@ -385,7 +385,9 @@ const __dirname = __dirname_fn(__filename); ) // Write the transpiled file with updated imports - const tempFile = filePath.replace(/\.ts$/, '.temp.mjs') + // Include process.pid + a random suffix so concurrent run-multiple workers + // don't write to and delete each other's temp files (see issue #5642). + const tempFile = filePath.replace(/\.ts$/, `.${process.pid}.${Math.random().toString(36).slice(2, 10)}.temp.mjs`) fs.writeFileSync(tempFile, jsContent) transpiledFiles.set(filePath, tempFile) } diff --git a/test/unit/utils/typescript_test.js b/test/unit/utils/typescript_test.js new file mode 100644 index 000000000..5f57ca71a --- /dev/null +++ b/test/unit/utils/typescript_test.js @@ -0,0 +1,36 @@ +import { expect } from 'chai' +import { fileURLToPath } from 'url' +import path from 'path' +import { createRequire } from 'module' +import { transpileTypeScript, cleanupTempFiles } from '../../../lib/utils/typescript.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const require = createRequire(import.meta.url) +const typescript = require('typescript') + +const configPath = path.resolve(__dirname, '../../data/typescript-config-imports/tests/api/codecept.conf.ts') + +describe('TypeScript transpilation', () => { + it('uses unique temp file names per invocation so concurrent run-multiple workers do not delete each other (#5642)', async () => { + const first = await transpileTypeScript(configPath, typescript) + const second = await transpileTypeScript(configPath, typescript) + + try { + expect(first.allTempFiles.length).to.be.greaterThan(0) + expect(second.allTempFiles.length).to.equal(first.allTempFiles.length) + + // Every temp file path is still recognisable as a transpiled file + for (const file of [...first.allTempFiles, ...second.allTempFiles]) { + expect(file).to.match(/\.temp\.mjs$/) + } + + // The two invocations must not share any temp file path, otherwise one + // worker's cleanup would remove files the other still needs to import. + const shared = first.allTempFiles.filter(f => second.allTempFiles.includes(f)) + expect(shared, `temp files were shared between invocations: ${shared}`).to.be.empty + } finally { + cleanupTempFiles(first.allTempFiles) + cleanupTempFiles(second.allTempFiles) + } + }) +})