Skip to content

Commit c63defa

Browse files
authored
Merge pull request #1984 from codeflash-ai/fix/js-project-root-per-function
Fix: Recalculate js_project_root per function in monorepos
2 parents 73d7a77 + e7596b5 commit c63defa

3 files changed

Lines changed: 142 additions & 3 deletions

File tree

codeflash/languages/function_optimizer.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3085,6 +3085,16 @@ def run_optimized_candidate(
30853085
)
30863086
)
30873087

3088+
def get_js_project_root(self) -> Path | None:
3089+
# Only calculate for JavaScript/TypeScript projects
3090+
if self.function_to_optimize.language not in ("javascript", "typescript"):
3091+
return self.test_cfg.js_project_root # Fall back to cached value for non-JS
3092+
3093+
# For JS/TS, calculate fresh for each function to support monorepos
3094+
from codeflash.languages.javascript.test_runner import find_node_project_root
3095+
3096+
return find_node_project_root(Path(self.function_to_optimize.file_path))
3097+
30883098
def run_and_parse_tests(
30893099
self,
30903100
testing_type: TestingMode,
@@ -3103,33 +3113,39 @@ def run_and_parse_tests(
31033113
coverage_config_file = None
31043114
try:
31053115
if testing_type == TestingMode.BEHAVIOR:
3116+
# Calculate js_project_root for the current function being optimized
3117+
# instead of using cached value from test_cfg, which may be from a different function
3118+
js_project_root = self.get_js_project_root()
3119+
31063120
result_file_path, run_result, coverage_database_file, coverage_config_file = (
31073121
self.language_support.run_behavioral_tests(
31083122
test_paths=test_files,
31093123
test_env=test_env,
31103124
cwd=self.project_root,
31113125
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3112-
project_root=self.test_cfg.js_project_root,
3126+
project_root=js_project_root,
31133127
enable_coverage=enable_coverage,
31143128
candidate_index=optimization_iteration,
31153129
)
31163130
)
31173131
elif testing_type == TestingMode.LINE_PROFILE:
3132+
js_project_root = self.get_js_project_root()
31183133
result_file_path, run_result = self.language_support.run_line_profile_tests(
31193134
test_paths=test_files,
31203135
test_env=test_env,
31213136
cwd=self.project_root,
31223137
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3123-
project_root=self.test_cfg.js_project_root,
3138+
project_root=js_project_root,
31243139
line_profile_output_file=line_profiler_output_file,
31253140
)
31263141
elif testing_type == TestingMode.PERFORMANCE:
3142+
js_project_root = self.get_js_project_root()
31273143
result_file_path, run_result = self.language_support.run_benchmarking_tests(
31283144
test_paths=test_files,
31293145
test_env=test_env,
31303146
cwd=self.project_root,
31313147
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3132-
project_root=self.test_cfg.js_project_root,
3148+
project_root=js_project_root,
31333149
min_loops=pytest_min_loops,
31343150
max_loops=pytest_max_loops,
31353151
target_duration_seconds=testing_time,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Test that js_project_root is recalculated per function, not cached."""
2+
3+
from pathlib import Path
4+
5+
from codeflash.languages.javascript.test_runner import find_node_project_root
6+
7+
8+
def test_find_node_project_root_returns_different_roots_for_different_files(tmp_path: Path) -> None:
9+
"""Test that find_node_project_root returns the correct root for each file."""
10+
# Create main project structure
11+
main_project = (tmp_path / "project").resolve()
12+
main_project.mkdir()
13+
(main_project / "package.json").write_text("{}", encoding="utf-8")
14+
(main_project / "src").mkdir()
15+
main_file = (main_project / "src" / "main.ts").resolve()
16+
main_file.write_text("// main file", encoding="utf-8")
17+
18+
# Create extension subdirectory with its own package.json
19+
extension_dir = (main_project / "extensions" / "discord").resolve()
20+
extension_dir.mkdir(parents=True)
21+
(extension_dir / "package.json").write_text("{}", encoding="utf-8")
22+
(extension_dir / "src").mkdir()
23+
extension_file = (extension_dir / "src" / "accounts.ts").resolve()
24+
extension_file.write_text("// extension file", encoding="utf-8")
25+
26+
# Extension file should return extension directory
27+
result1 = find_node_project_root(extension_file)
28+
assert result1 == extension_dir, f"Expected {extension_dir}, got {result1}"
29+
30+
# Main file should return main project directory
31+
result2 = find_node_project_root(main_file)
32+
assert result2 == main_project, f"Expected {main_project}, got {result2}"
33+
34+
# Calling again with extension file should still return extension dir
35+
result3 = find_node_project_root(extension_file)
36+
assert result3 == extension_dir, f"Expected {extension_dir}, got {result3}"
37+
38+
39+
def test_js_project_root_recalculated_per_function(tmp_path: Path) -> None:
40+
"""Each function in a monorepo should resolve to its own nearest package.json root."""
41+
# Create main project
42+
main_project = (tmp_path / "project").resolve()
43+
main_project.mkdir()
44+
(main_project / "package.json").write_text('{"name": "main"}', encoding="utf-8")
45+
(main_project / "src").mkdir()
46+
47+
# Create extension with its own package.json
48+
extension_dir = (main_project / "extensions" / "discord").resolve()
49+
extension_dir.mkdir(parents=True)
50+
(extension_dir / "package.json").write_text('{"name": "discord-extension"}', encoding="utf-8")
51+
(extension_dir / "src").mkdir()
52+
53+
extension_file = (extension_dir / "src" / "accounts.ts").resolve()
54+
extension_file.write_text("export function foo() {}", encoding="utf-8")
55+
56+
main_file = (main_project / "src" / "commands.ts").resolve()
57+
main_file.write_text("export function bar() {}", encoding="utf-8")
58+
59+
js_project_root_1 = find_node_project_root(extension_file)
60+
assert js_project_root_1 == extension_dir
61+
62+
js_project_root_2 = find_node_project_root(main_file)
63+
assert js_project_root_2 == main_project, (
64+
f"Expected {main_project}, got {js_project_root_2}. "
65+
f"Happens when js_project_root is not recalculated per function."
66+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Test that test_cfg.js_project_root caching bug is demonstrated and bypassed by the fix."""
2+
3+
from pathlib import Path
4+
from unittest.mock import patch
5+
6+
from codeflash.languages.javascript.support import JavaScriptSupport
7+
from codeflash.verification.verification_utils import TestConfig
8+
9+
10+
@patch("codeflash.languages.javascript.optimizer.verify_js_requirements")
11+
def test_js_project_root_cached_in_test_cfg(mock_verify: object, tmp_path: Path) -> None:
12+
"""Demonstrates that test_cfg.js_project_root is set once per setup_test_config call.
13+
14+
This test shows the root cause: test_cfg caches the project root from the first function.
15+
The fix bypasses this cache in FunctionOptimizer.get_js_project_root() instead of
16+
changing how test_cfg stores the value.
17+
"""
18+
mock_verify.return_value = [] # type: ignore[attr-defined]
19+
20+
# Create main project
21+
main_project = (tmp_path / "project").resolve()
22+
main_project.mkdir()
23+
(main_project / "package.json").write_text('{"name": "main"}', encoding="utf-8")
24+
(main_project / "src").mkdir()
25+
(main_project / "test").mkdir()
26+
(main_project / "node_modules").mkdir()
27+
28+
# Create extension with its own package.json
29+
extension_dir = (main_project / "extensions" / "discord").resolve()
30+
extension_dir.mkdir(parents=True)
31+
(extension_dir / "package.json").write_text('{"name": "discord-extension"}', encoding="utf-8")
32+
(extension_dir / "src").mkdir()
33+
(extension_dir / "node_modules").mkdir()
34+
35+
test_cfg = TestConfig(
36+
tests_root=main_project / "test",
37+
project_root_path=main_project,
38+
tests_project_rootdir=main_project / "test",
39+
)
40+
test_cfg.set_language("javascript")
41+
42+
js_support = JavaScriptSupport()
43+
44+
extension_file = (extension_dir / "src" / "accounts.ts").resolve()
45+
extension_file.write_text("export function foo() {}", encoding="utf-8")
46+
47+
success = js_support.setup_test_config(test_cfg, extension_file, current_worktree=None)
48+
assert success, "setup_test_config should succeed"
49+
# After setup for extension file, js_project_root is the extension directory
50+
assert test_cfg.js_project_root == extension_dir
51+
52+
# test_cfg is NOT re-initialized for subsequent functions — js_project_root stays cached
53+
main_file = (main_project / "src" / "commands.ts").resolve()
54+
main_file.write_text("export function bar() {}", encoding="utf-8")
55+
56+
# The cached value is still extension_dir, not main_project — this is the root cause
57+
assert test_cfg.js_project_root == extension_dir

0 commit comments

Comments
 (0)