Skip to content

Commit 4c61d08

Browse files
Merge branch 'main' into fix/js-jest30-loop-runner
2 parents 04a87cf + 2f19cdd commit 4c61d08

18 files changed

Lines changed: 588 additions & 240 deletions

File tree

codeflash/cli_cmds/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,13 @@ def _handle_show_config() -> None:
359359
detected = detect_project(project_root)
360360

361361
# Check if config exists or is auto-detected
362-
config_exists, _ = has_existing_config(project_root)
362+
config_exists, config_file = has_existing_config(project_root)
363363
status = "Saved config" if config_exists else "Auto-detected (not saved)"
364364

365365
console.print()
366366
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
367+
if config_exists and config_file:
368+
console.print(f"[dim]Config file: {project_root / config_file}[/dim]")
367369
console.print()
368370

369371
table = Table(show_header=True, header_style="bold cyan")

codeflash/code_utils/config_js.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from pathlib import Path
77
from typing import Any
88

9+
from codeflash.setup.detector import is_build_output_dir
10+
911
PACKAGE_JSON_CACHE: dict[Path, Path] = {}
1012
PACKAGE_JSON_DATA_CACHE: dict[Path, dict[str, Any]] = {}
1113

@@ -50,12 +52,15 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
5052
"""Detect module root from package.json fields or directory conventions.
5153
5254
Detection order:
53-
1. package.json "exports" field (extract directory from main export)
54-
2. package.json "module" field (ESM entry point)
55-
3. package.json "main" field (CJS entry point)
56-
4. "src/" directory if it exists
55+
1. src/, lib/, source/ directories (common source directories)
56+
2. package.json "exports" field (if not in build output directory)
57+
3. package.json "module" field (ESM, if not in build output directory)
58+
4. package.json "main" field (CJS, if not in build output directory)
5759
5. Fall back to "." (project root)
5860
61+
Build output directories (build/, dist/, out/) are skipped since they contain
62+
compiled code, not source files.
63+
5964
Args:
6065
project_root: Root directory of the project.
6166
package_data: Parsed package.json data.
@@ -64,6 +69,11 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
6469
Detected module root path (relative to project root).
6570
6671
"""
72+
# Check for common source directories first - these are always preferred
73+
for src_dir in ["src", "lib", "source"]:
74+
if (project_root / src_dir).is_dir():
75+
return src_dir
76+
6777
# Check exports field (modern Node.js)
6878
exports = package_data.get("exports")
6979
if exports:
@@ -80,27 +90,38 @@ def detect_module_root(project_root: Path, package_data: dict[str, Any]) -> str:
8090

8191
if entry_path and isinstance(entry_path, str):
8292
parent = Path(entry_path).parent
83-
if parent != Path() and (project_root / parent).is_dir():
93+
if (
94+
parent != Path()
95+
and parent.as_posix() != "."
96+
and (project_root / parent).is_dir()
97+
and not is_build_output_dir(parent)
98+
):
8499
return parent.as_posix()
85100

86101
# Check module field (ESM)
87102
module_field = package_data.get("module")
88103
if module_field and isinstance(module_field, str):
89104
parent = Path(module_field).parent
90-
if parent != Path() and (project_root / parent).is_dir():
105+
if (
106+
parent != Path()
107+
and parent.as_posix() != "."
108+
and (project_root / parent).is_dir()
109+
and not is_build_output_dir(parent)
110+
):
91111
return parent.as_posix()
92112

93113
# Check main field (CJS)
94114
main_field = package_data.get("main")
95115
if main_field and isinstance(main_field, str):
96116
parent = Path(main_field).parent
97-
if parent != Path() and (project_root / parent).is_dir():
117+
if (
118+
parent != Path()
119+
and parent.as_posix() != "."
120+
and (project_root / parent).is_dir()
121+
and not is_build_output_dir(parent)
122+
):
98123
return parent.as_posix()
99124

100-
# Check for src/ directory convention
101-
if (project_root / "src").is_dir():
102-
return "src"
103-
104125
# Default to project root
105126
return "."
106127

codeflash/languages/javascript/module_system.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def _convert_destructuring_to_imports(names_str: str) -> str:
222222
223223
Returns:
224224
Import names string with aliases using 'as' syntax
225+
225226
"""
226227
# Split by commas and process each name
227228
parts = []

codeflash/languages/javascript/test_runner.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,33 @@ def _find_node_project_root(file_path: Path) -> Path | None:
296296
return None
297297

298298

299+
def _find_monorepo_root(start_path: Path) -> Path | None:
300+
"""Find the monorepo workspace root by looking for workspace markers.
301+
302+
Traverses up from the given path to find a directory containing
303+
monorepo workspace markers like yarn.lock, pnpm-workspace.yaml, etc.
304+
305+
Args:
306+
start_path: A path within the monorepo.
307+
308+
Returns:
309+
The monorepo root directory, or None if not found.
310+
311+
"""
312+
monorepo_markers = ["yarn.lock", "pnpm-workspace.yaml", "lerna.json", "package-lock.json"]
313+
current = start_path if start_path.is_dir() else start_path.parent
314+
315+
while current != current.parent:
316+
# Check for monorepo markers
317+
if any((current / marker).exists() for marker in monorepo_markers):
318+
# Verify it has node_modules (it's the workspace root)
319+
if (current / "node_modules").exists():
320+
return current
321+
current = current.parent
322+
323+
return None
324+
325+
299326
def _find_jest_config(project_root: Path) -> Path | None:
300327
"""Find Jest configuration file in the project.
301328
@@ -797,6 +824,12 @@ def run_jest_benchmarking_tests(
797824
jest_env["JEST_JUNIT_SUITE_NAME"] = "{filepath}"
798825
jest_env["JEST_JUNIT_ADD_FILE_ATTRIBUTE"] = "true"
799826
jest_env["JEST_JUNIT_INCLUDE_CONSOLE_OUTPUT"] = "true"
827+
828+
# Pass monorepo root to loop-runner for jest-runner resolution
829+
monorepo_root = _find_monorepo_root(effective_cwd)
830+
if monorepo_root:
831+
jest_env["CODEFLASH_MONOREPO_ROOT"] = str(monorepo_root)
832+
logger.debug(f"Detected monorepo root: {monorepo_root}")
800833
codeflash_sqlite_file = get_run_tmp_file(Path("test_return_values_0.sqlite"))
801834
jest_env["CODEFLASH_OUTPUT_FILE"] = str(codeflash_sqlite_file)
802835
jest_env["CODEFLASH_TEST_ITERATION"] = "0"

codeflash/setup/detector.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
import tomlkit
2323

24+
_BUILD_DIRS = frozenset({"build", "dist", "out", ".next", ".nuxt"})
25+
2426

2527
@dataclass
2628
class DetectedProject:
@@ -310,14 +312,21 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
310312
"""Detect JavaScript/TypeScript module root.
311313
312314
Priority:
313-
1. package.json "exports" field
314-
2. package.json "module" field (ESM)
315-
3. package.json "main" field (CJS)
316-
4. src/ directory
317-
5. lib/ directory
318-
6. Project root
315+
1. src/, lib/, source/ directories (common source directories)
316+
2. package.json "exports" field (if not in build output directory)
317+
3. package.json "module" field (ESM, if not in build output directory)
318+
4. package.json "main" field (CJS, if not in build output directory)
319+
5. Project root
320+
321+
Build output directories (build/, dist/, out/) are skipped since they contain
322+
compiled code, not source files.
319323
320324
"""
325+
# Check for common source directories first - these are always preferred
326+
for src_dir in ["src", "lib", "source"]:
327+
if (project_root / src_dir).is_dir():
328+
return project_root / src_dir, f"{src_dir}/ directory"
329+
321330
package_json_path = project_root / "package.json"
322331
package_data: dict[str, Any] = {}
323332

@@ -334,32 +343,52 @@ def _detect_js_module_root(project_root: Path) -> tuple[Path, str]:
334343
entry_path = _extract_entry_path(exports)
335344
if entry_path:
336345
parent = Path(entry_path).parent
337-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
346+
if (
347+
parent != Path()
348+
and parent.as_posix() != "."
349+
and (project_root / parent).is_dir()
350+
and not is_build_output_dir(parent)
351+
):
338352
return project_root / parent, f'{parent.as_posix()}/ (from package.json "exports")'
339353

340354
# Check module field (ESM)
341355
module_field = package_data.get("module")
342356
if module_field and isinstance(module_field, str):
343357
parent = Path(module_field).parent
344-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
358+
if (
359+
parent != Path()
360+
and parent.as_posix() != "."
361+
and (project_root / parent).is_dir()
362+
and not is_build_output_dir(parent)
363+
):
345364
return project_root / parent, f'{parent.as_posix()}/ (from package.json "module")'
346365

347366
# Check main field (CJS)
348367
main_field = package_data.get("main")
349368
if main_field and isinstance(main_field, str):
350369
parent = Path(main_field).parent
351-
if parent != Path() and parent.as_posix() != "." and (project_root / parent).is_dir():
370+
if (
371+
parent != Path()
372+
and parent.as_posix() != "."
373+
and (project_root / parent).is_dir()
374+
and not is_build_output_dir(parent)
375+
):
352376
return project_root / parent, f'{parent.as_posix()}/ (from package.json "main")'
353377

354-
# Check for common source directories
355-
for src_dir in ["src", "lib", "source"]:
356-
if (project_root / src_dir).is_dir():
357-
return project_root / src_dir, f"{src_dir}/ directory"
358-
359378
# Default to project root
360379
return project_root, "project root"
361380

362381

382+
def is_build_output_dir(path: Path) -> bool:
383+
"""Check if a path is within a common build output directory.
384+
385+
Build output directories contain compiled code and should be skipped
386+
in favor of source directories.
387+
388+
"""
389+
return not _BUILD_DIRS.isdisjoint(path.parts)
390+
391+
363392
def _extract_entry_path(exports: Any) -> str | None:
364393
"""Extract entry path from package.json exports field."""
365394
if isinstance(exports, str):

codeflash/verification/parse_test_output.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,31 @@ def resolve_test_file_from_class_path(test_class_path: str, base_dir: Path) -> P
150150
# Handle file paths (contain slashes and extensions like .js/.ts)
151151
if "/" in test_class_path or "\\" in test_class_path:
152152
# This is a file path, not a Python module path
153+
# Try the path as-is if it's absolute
154+
potential_path = Path(test_class_path)
155+
if potential_path.is_absolute() and potential_path.exists():
156+
return potential_path
157+
153158
# Try to resolve relative to base_dir's parent (project root)
154159
project_root = base_dir.parent
155160
potential_path = project_root / test_class_path
156-
if potential_path.exists():
157-
return potential_path
161+
# Normalize to resolve .. and . components
162+
try:
163+
potential_path = potential_path.resolve()
164+
if potential_path.exists():
165+
return potential_path
166+
except (OSError, RuntimeError):
167+
pass
168+
158169
# Also try relative to base_dir itself
159170
potential_path = base_dir / test_class_path
160-
if potential_path.exists():
161-
return potential_path
162-
# Try the path as-is if it's absolute
163-
potential_path = Path(test_class_path)
164-
if potential_path.exists():
165-
return potential_path
171+
try:
172+
potential_path = potential_path.resolve()
173+
if potential_path.exists():
174+
return potential_path
175+
except (OSError, RuntimeError):
176+
pass
177+
166178
return None
167179

168180
# First try the full path (Python module path)
@@ -731,16 +743,25 @@ def parse_jest_test_xml(
731743
if not test_file_path.exists():
732744
test_file_path = base_dir / test_file_name
733745

734-
if test_file_path is None or not test_file_path.exists():
746+
# For Jest tests in monorepos, test files may not exist after cleanup
747+
# but we can still parse results and infer test type from the path
748+
if test_file_path is None:
735749
logger.warning(f"Could not resolve test file for Jest test: {test_class_path}")
736750
continue
737751

738752
# Get test type if not already set from lookup
739-
if test_type is None:
753+
if test_type is None and test_file_path.exists():
740754
test_type = test_files.get_test_type_by_instrumented_file_path(test_file_path)
741755
if test_type is None:
742-
# Default to GENERATED_REGRESSION for Jest tests
743-
test_type = TestType.GENERATED_REGRESSION
756+
# Infer test type from filename pattern
757+
filename = test_file_path.name
758+
if "__perf_test_" in filename or "_perf_test_" in filename:
759+
test_type = TestType.GENERATED_PERFORMANCE
760+
elif "__unit_test_" in filename or "_unit_test_" in filename:
761+
test_type = TestType.GENERATED_REGRESSION
762+
else:
763+
# Default to GENERATED_REGRESSION for Jest tests
764+
test_type = TestType.GENERATED_REGRESSION
744765

745766
# For Jest tests, keep the relative file path with extension intact
746767
# (Python uses module_name_from_file_path which strips extensions)

npm-package/README.md

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)