Skip to content

Commit d616501

Browse files
Merge pull request #1808 from codeflash-ai/fix/jest-runtime-config-for-external-tests
fix: use runtime Jest config for test files outside project root
2 parents 7f7591c + eeeb6ee commit d616501

2 files changed

Lines changed: 171 additions & 125 deletions

File tree

codeflash/languages/javascript/test_runner.py

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,73 @@ def _create_codeflash_jest_config(
339339
return None
340340

341341

342+
def _create_runtime_jest_config(base_config_path: Path | None, project_root: Path, test_dirs: set[str]) -> Path | None:
343+
"""Create a runtime Jest config that includes test directories in roots and testMatch.
344+
345+
This is needed because test files generated by codeflash may be placed
346+
outside the project root (e.g., in a monorepo where the source file lives
347+
in a subpackage but tests are generated at the repo root). Jest requires
348+
test files to be within configured ``roots`` and to match ``testMatch``
349+
patterns (which typically use ``<rootDir>``). Since ``roots`` set via CLI
350+
can be overridden by config, and ``testMatch`` patterns using ``<rootDir>``
351+
won't match files outside the project root, we must create a wrapper config.
352+
353+
Args:
354+
base_config_path: Path to the base Jest config to extend, or None.
355+
project_root: The project root directory (where package.json lives).
356+
test_dirs: Set of absolute directory paths containing test files.
357+
358+
Returns:
359+
Path to the created runtime config file.
360+
361+
"""
362+
is_esm = _is_esm_project(project_root)
363+
config_ext = ".cjs" if is_esm else ".js"
364+
365+
if base_config_path:
366+
config_dir = base_config_path.parent
367+
else:
368+
config_dir = project_root
369+
370+
runtime_config_path = config_dir / f"jest.codeflash.runtime.config{config_ext}"
371+
372+
test_dirs_js = ", ".join(f"'{d}'" for d in sorted(test_dirs))
373+
374+
if base_config_path:
375+
require_path = f"./{base_config_path.name}"
376+
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
377+
const baseConfig = require('{require_path}');
378+
module.exports = {{
379+
...baseConfig,
380+
roots: [
381+
...(baseConfig.roots || [__dirname]),
382+
{test_dirs_js},
383+
],
384+
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
385+
}};
386+
"""
387+
else:
388+
config_content = f"""// Auto-generated by codeflash - runtime config with test roots
389+
module.exports = {{
390+
roots: ['{project_root}', {test_dirs_js}],
391+
testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.test.tsx', '**/*.test.jsx'],
392+
}};
393+
"""
394+
395+
try:
396+
runtime_config_path.write_text(config_content, encoding="utf-8")
397+
_created_config_files.add(runtime_config_path)
398+
logger.debug(f"Created runtime Jest config with test roots: {runtime_config_path}")
399+
except Exception as e:
400+
logger.warning(f"Failed to create runtime Jest config: {e}")
401+
# Fall back to base config
402+
if base_config_path:
403+
return base_config_path
404+
return None
405+
406+
return runtime_config_path
407+
408+
342409
def _get_jest_config_for_project(project_root: Path) -> Path | None:
343410
"""Get the appropriate Jest config for the project.
344411
@@ -712,6 +779,17 @@ def run_jest_behavioral_tests(
712779
# Add Jest config if found - needed for TypeScript transformation
713780
# Uses codeflash-compatible config if project has bundler moduleResolution
714781
jest_config = _get_jest_config_for_project(effective_cwd)
782+
783+
# If test files are outside the project root, create a runtime wrapper config
784+
# that adds their directories to Jest's `roots` and overrides `testMatch`.
785+
# This is necessary because Jest's testMatch patterns use <rootDir> which
786+
# resolves to the config file's directory, excluding external test files.
787+
if test_files:
788+
resolved_root = effective_cwd.resolve()
789+
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
790+
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
791+
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
792+
715793
if jest_config:
716794
jest_cmd.append(f"--config={jest_config}")
717795

@@ -723,12 +801,6 @@ def run_jest_behavioral_tests(
723801
jest_cmd.append("--runTestsByPath")
724802
resolved_test_files = [str(Path(f).resolve()) for f in test_files]
725803
jest_cmd.extend(resolved_test_files)
726-
# Add --roots to include directories containing test files
727-
# This is needed because some projects configure Jest with restricted roots
728-
# (e.g., roots: ["<rootDir>/src"]) which excludes the test directory
729-
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
730-
for test_dir in sorted(test_dirs):
731-
jest_cmd.extend(["--roots", test_dir])
732804

733805
if timeout:
734806
jest_cmd.append(f"--testTimeout={timeout * 1000}") # Jest uses milliseconds
@@ -962,17 +1034,21 @@ def run_jest_benchmarking_tests(
9621034
# Add Jest config if found - needed for TypeScript transformation
9631035
# Uses codeflash-compatible config if project has bundler moduleResolution
9641036
jest_config = _get_jest_config_for_project(effective_cwd)
1037+
1038+
# If test files are outside the project root, create a runtime wrapper config
1039+
if test_files:
1040+
resolved_root = effective_cwd.resolve()
1041+
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
1042+
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
1043+
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
1044+
9651045
if jest_config:
9661046
jest_cmd.append(f"--config={jest_config}")
9671047

9681048
if test_files:
9691049
jest_cmd.append("--runTestsByPath")
9701050
resolved_test_files = [str(Path(f).resolve()) for f in test_files]
9711051
jest_cmd.extend(resolved_test_files)
972-
# Add --roots to include directories containing test files
973-
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
974-
for test_dir in sorted(test_dirs):
975-
jest_cmd.extend(["--roots", test_dir])
9761052

9771053
if timeout:
9781054
jest_cmd.append(f"--testTimeout={timeout * 1000}")
@@ -1127,17 +1203,21 @@ def run_jest_line_profile_tests(
11271203
# Add Jest config if found - needed for TypeScript transformation
11281204
# Uses codeflash-compatible config if project has bundler moduleResolution
11291205
jest_config = _get_jest_config_for_project(effective_cwd)
1206+
1207+
# If test files are outside the project root, create a runtime wrapper config
1208+
if test_files:
1209+
resolved_root = effective_cwd.resolve()
1210+
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
1211+
if any(not Path(d).is_relative_to(resolved_root) for d in test_dirs):
1212+
jest_config = _create_runtime_jest_config(jest_config, effective_cwd, test_dirs)
1213+
11301214
if jest_config:
11311215
jest_cmd.append(f"--config={jest_config}")
11321216

11331217
if test_files:
11341218
jest_cmd.append("--runTestsByPath")
11351219
resolved_test_files = [str(Path(f).resolve()) for f in test_files]
11361220
jest_cmd.extend(resolved_test_files)
1137-
# Add --roots to include directories containing test files
1138-
test_dirs = {str(Path(f).resolve().parent) for f in test_files}
1139-
for test_dir in sorted(test_dirs):
1140-
jest_cmd.extend(["--roots", test_dir])
11411221

11421222
if timeout:
11431223
jest_cmd.append(f"--testTimeout={timeout * 1000}")

0 commit comments

Comments
 (0)