Skip to content

Commit 46c4991

Browse files
Fix: Recalculate js_project_root per function in monorepos
**Issue:** When optimizing multiple functions in a monorepo with nested package.json files (e.g., extensions/discord/package.json), the js_project_root was set once for the first function and reused for all subsequent functions. This caused vitest to look for setupFiles in the wrong directory. **Root Cause:** test_cfg.js_project_root was set during initial setup and never recalculated. When function #1 was in extensions/discord/, all subsequent functions in src/ inherited this wrong project root. **Fix:** - Added _get_js_project_root() method to FunctionOptimizer - Calculate js_project_root fresh for each function using find_node_project_root() - Updated all test execution paths (behavior, performance, line_profile) **Impact:** - Vitest now runs from the correct working directory for each function - setupFiles can be resolved correctly - Functions in different monorepo packages can be optimized correctly Fixes trace IDs: 12d26b00-cbae-49a8-a3cd-c36024ee06ec, 1cde1c65-ef42-4072-afbc-165b0c235688, and 18 others Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent a756701 commit 46c4991

3 files changed

Lines changed: 276 additions & 3 deletions

File tree

codeflash/languages/function_optimizer.py

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

3088+
def _get_js_project_root(self) -> Path | None:
3089+
"""Get the JavaScript project root for the current function being optimized.
3090+
3091+
This method calculates the js_project_root for each function instead of
3092+
caching it in test_cfg. This is important in monorepos where different
3093+
functions may belong to different packages/extensions with their own
3094+
package.json files.
3095+
3096+
Returns:
3097+
Path to the JavaScript project root, or None if not a JavaScript project
3098+
or if the project root cannot be determined.
3099+
"""
3100+
# Only calculate for JavaScript/TypeScript projects
3101+
if self.function_to_optimize.language not in ("javascript", "typescript"):
3102+
return self.test_cfg.js_project_root # Fall back to cached value for non-JS
3103+
3104+
# For JS/TS, calculate fresh for each function
3105+
from pathlib import Path
3106+
3107+
from codeflash.languages.javascript.test_runner import find_node_project_root
3108+
3109+
source_file = Path(self.function_to_optimize.file_path)
3110+
return find_node_project_root(source_file)
3111+
30883112
def run_and_parse_tests(
30893113
self,
30903114
testing_type: TestingMode,
@@ -3103,33 +3127,39 @@ def run_and_parse_tests(
31033127
coverage_config_file = None
31043128
try:
31053129
if testing_type == TestingMode.BEHAVIOR:
3130+
# Calculate js_project_root for the current function being optimized
3131+
# instead of using cached value from test_cfg, which may be from a different function
3132+
js_project_root = self._get_js_project_root()
3133+
31063134
result_file_path, run_result, coverage_database_file, coverage_config_file = (
31073135
self.language_support.run_behavioral_tests(
31083136
test_paths=test_files,
31093137
test_env=test_env,
31103138
cwd=self.project_root,
31113139
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3112-
project_root=self.test_cfg.js_project_root,
3140+
project_root=js_project_root,
31133141
enable_coverage=enable_coverage,
31143142
candidate_index=optimization_iteration,
31153143
)
31163144
)
31173145
elif testing_type == TestingMode.LINE_PROFILE:
3146+
js_project_root = self._get_js_project_root()
31183147
result_file_path, run_result = self.language_support.run_line_profile_tests(
31193148
test_paths=test_files,
31203149
test_env=test_env,
31213150
cwd=self.project_root,
31223151
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3123-
project_root=self.test_cfg.js_project_root,
3152+
project_root=js_project_root,
31243153
line_profile_output_file=line_profiler_output_file,
31253154
)
31263155
elif testing_type == TestingMode.PERFORMANCE:
3156+
js_project_root = self._get_js_project_root()
31273157
result_file_path, run_result = self.language_support.run_benchmarking_tests(
31283158
test_paths=test_files,
31293159
test_env=test_env,
31303160
cwd=self.project_root,
31313161
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3132-
project_root=self.test_cfg.js_project_root,
3162+
project_root=js_project_root,
31333163
min_loops=pytest_min_loops,
31343164
max_loops=pytest_max_loops,
31353165
target_duration_seconds=testing_time,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Test that js_project_root is recalculated per function, not cached."""
2+
3+
import tempfile
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from codeflash.languages.javascript.test_runner import find_node_project_root
9+
10+
11+
def test_find_node_project_root_returns_different_roots_for_different_files():
12+
"""Test that find_node_project_root returns the correct root for each file."""
13+
with tempfile.TemporaryDirectory() as tmpdir:
14+
root = Path(tmpdir)
15+
16+
# Create main project structure
17+
main_project = root / "project"
18+
main_project.mkdir()
19+
(main_project / "package.json").write_text("{}")
20+
(main_project / "src").mkdir()
21+
main_file = main_project / "src" / "main.ts"
22+
main_file.write_text("// main file")
23+
24+
# Create extension subdirectory with its own package.json
25+
extension_dir = main_project / "extensions" / "discord"
26+
extension_dir.mkdir(parents=True)
27+
(extension_dir / "package.json").write_text("{}")
28+
(extension_dir / "src").mkdir()
29+
extension_file = extension_dir / "src" / "accounts.ts"
30+
extension_file.write_text("// extension file")
31+
32+
# Test 1: Extension file should return extension directory
33+
result1 = find_node_project_root(extension_file)
34+
assert result1 == extension_dir, (
35+
f"Expected {extension_dir}, got {result1}"
36+
)
37+
38+
# Test 2: Main file should return main project directory
39+
result2 = find_node_project_root(main_file)
40+
assert result2 == main_project, (
41+
f"Expected {main_project}, got {result2}"
42+
)
43+
44+
# Test 3: Calling again with extension file should still return extension dir
45+
result3 = find_node_project_root(extension_file)
46+
assert result3 == extension_dir, (
47+
f"Expected {extension_dir}, got {result3}"
48+
)
49+
50+
51+
def test_js_project_root_should_be_recalculated_per_function():
52+
"""
53+
Test the actual bug: when optimizing multiple functions from different
54+
directories, each should get its own js_project_root, not inherit from
55+
the first function.
56+
57+
This test simulates the scenario where:
58+
1. Function #1 is in extensions/discord/src/accounts.ts
59+
2. Function #2 is in src/plugins/commands.ts
60+
3. Both should get their correct respective project roots
61+
"""
62+
with tempfile.TemporaryDirectory() as tmpdir:
63+
root = Path(tmpdir)
64+
65+
# Create main project
66+
main_project = root / "project"
67+
main_project.mkdir()
68+
(main_project / "package.json").write_text('{"name": "main"}')
69+
(main_project / "src").mkdir()
70+
(main_project / "test").mkdir()
71+
72+
# Create extension with its own package.json
73+
extension_dir = main_project / "extensions" / "discord"
74+
extension_dir.mkdir(parents=True)
75+
(extension_dir / "package.json").write_text('{"name": "discord-extension"}')
76+
(extension_dir / "src").mkdir()
77+
78+
# Files to optimize
79+
extension_file = extension_dir / "src" / "accounts.ts"
80+
extension_file.write_text("export function foo() {}")
81+
82+
main_file = main_project / "src" / "commands.ts"
83+
main_file.write_text("export function bar() {}")
84+
85+
# Simulate what happens in Codeflash optimizer
86+
# Function 1 (extension file) sets js_project_root
87+
js_project_root_1 = find_node_project_root(extension_file)
88+
assert js_project_root_1 == extension_dir
89+
90+
# Function 2 (main file) should get its own root, not inherit from function 1
91+
js_project_root_2 = find_node_project_root(main_file)
92+
assert js_project_root_2 == main_project, (
93+
f"Bug reproduced: main file got {js_project_root_2} instead of {main_project}. "
94+
f"This happens when test_cfg.js_project_root is not recalculated per function."
95+
)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""
2+
Test for the bug where test_cfg.js_project_root is set once and reused.
3+
4+
The bug: When optimizing multiple functions from different directories in a monorepo,
5+
the js_project_root from the FIRST function is cached in test_cfg and used for ALL
6+
subsequent functions, causing incorrect vitest working directories.
7+
"""
8+
9+
import tempfile
10+
from pathlib import Path
11+
from unittest.mock import MagicMock, patch
12+
13+
import pytest
14+
15+
from codeflash.languages.javascript.support import JavaScriptSupport
16+
from codeflash.verification.verification_utils import TestConfig
17+
18+
19+
@patch("codeflash.languages.javascript.optimizer.verify_js_requirements")
20+
def test_js_project_root_not_recalculated_demonstrates_bug(mock_verify):
21+
"""
22+
This test demonstrates the bug where js_project_root is set once
23+
and never updated when optimizing functions from different directories.
24+
25+
Expected behavior: Each function should get its own js_project_root
26+
Actual behavior: All functions share the first function's js_project_root
27+
"""
28+
# Mock verify_js_requirements to always pass
29+
mock_verify.return_value = []
30+
31+
with tempfile.TemporaryDirectory() as tmpdir:
32+
root = Path(tmpdir)
33+
34+
# Create main project
35+
main_project = root / "project"
36+
main_project.mkdir()
37+
(main_project / "package.json").write_text('{"name": "main"}')
38+
(main_project / "src").mkdir()
39+
(main_project / "test").mkdir()
40+
(main_project / "node_modules").mkdir() # Add node_modules to pass requirements check
41+
42+
# Create extension with its own package.json
43+
extension_dir = main_project / "extensions" / "discord"
44+
extension_dir.mkdir(parents=True)
45+
(extension_dir / "package.json").write_text('{"name": "discord-extension"}')
46+
(extension_dir / "src").mkdir()
47+
(extension_dir / "node_modules").mkdir() # Add node_modules to pass requirements check
48+
49+
# Create test config (shared across all functions, simulating optimizer behavior)
50+
test_cfg = TestConfig(
51+
tests_root=main_project / "test",
52+
project_root_path=main_project,
53+
tests_project_rootdir=main_project / "test",
54+
)
55+
test_cfg.set_language("javascript")
56+
57+
# Create JavaScript support instance
58+
js_support = JavaScriptSupport()
59+
60+
# Optimize function 1 (in extension directory)
61+
extension_file = extension_dir / "src" / "accounts.ts"
62+
extension_file.write_text("export function foo() {}")
63+
64+
success = js_support.setup_test_config(test_cfg, extension_file, current_worktree=None)
65+
assert success, "setup_test_config should succeed"
66+
js_project_root_after_func1 = test_cfg.js_project_root
67+
68+
# Should be extension directory
69+
assert js_project_root_after_func1 == extension_dir, (
70+
f"Function 1: Expected {extension_dir}, got {js_project_root_after_func1}"
71+
)
72+
73+
# Optimize function 2 (in main src directory)
74+
main_file = main_project / "src" / "commands.ts"
75+
main_file.write_text("export function bar() {}")
76+
77+
# This is the bug: setup_test_config is NOT called again in the real code!
78+
# The test_cfg object is reused, so js_project_root stays as extension_dir
79+
80+
# In the real optimizer, test_cfg is reused without calling setup_test_config again
81+
# So js_project_root remains the same from function 1
82+
js_project_root_for_func2 = test_cfg.js_project_root
83+
84+
# BUG: This assertion should fail because js_project_root was not recalculated
85+
# It's still pointing to extension_dir instead of main_project
86+
assert js_project_root_for_func2 == extension_dir, (
87+
f"BUG DEMONSTRATED: Function 2 inherits function 1's js_project_root. "
88+
f"Expected {main_project}, got {js_project_root_for_func2}"
89+
)
90+
91+
# What SHOULD happen:
92+
# js_support.setup_test_config(test_cfg, main_file, current_worktree=None)
93+
# correct_root = test_cfg.js_project_root
94+
# assert correct_root == main_project
95+
96+
97+
@pytest.mark.xfail(reason="Demonstrates the bug - will fail once bug is fixed")
98+
@patch("codeflash.languages.javascript.optimizer.verify_js_requirements")
99+
def test_js_project_root_reused_across_functions_wrong_behavior(mock_verify):
100+
"""
101+
This test is marked xfail because it currently PASSES (demonstrating the bug).
102+
Once the bug is fixed, this test will FAIL (which is correct), and we can remove xfail.
103+
"""
104+
# Mock verify_js_requirements to always pass
105+
mock_verify.return_value = []
106+
107+
with tempfile.TemporaryDirectory() as tmpdir:
108+
root = Path(tmpdir)
109+
110+
main_project = root / "project"
111+
main_project.mkdir()
112+
(main_project / "package.json").write_text('{"name": "main"}')
113+
(main_project / "src").mkdir()
114+
(main_project / "test").mkdir()
115+
(main_project / "node_modules").mkdir()
116+
117+
extension_dir = main_project / "extensions" / "discord"
118+
extension_dir.mkdir(parents=True)
119+
(extension_dir / "package.json").write_text('{"name": "discord"}')
120+
(extension_dir / "src").mkdir()
121+
(extension_dir / "node_modules").mkdir()
122+
123+
test_cfg = TestConfig(
124+
tests_root=main_project / "test",
125+
project_root_path=main_project,
126+
tests_project_rootdir=main_project / "test",
127+
)
128+
test_cfg.set_language("javascript")
129+
130+
js_support = JavaScriptSupport()
131+
132+
# Set up for extension file
133+
extension_file = extension_dir / "src" / "accounts.ts"
134+
extension_file.write_text("export function foo() {}")
135+
js_support.setup_test_config(test_cfg, extension_file, current_worktree=None)
136+
137+
# Now try to use test_cfg for a different file
138+
main_file = main_project / "src" / "commands.ts"
139+
main_file.write_text("export function bar() {}")
140+
141+
# This assertion will PASS (showing the bug) because js_project_root is wrong
142+
# Once fixed, this will FAIL because js_project_root will be recalculated
143+
assert test_cfg.js_project_root == extension_dir, (
144+
"Bug exists: js_project_root is not recalculated per function"
145+
)
146+
147+
# The correct behavior would be:
148+
# assert test_cfg.js_project_root == main_project

0 commit comments

Comments
 (0)