diff --git a/codeflash/languages/function_optimizer.py b/codeflash/languages/function_optimizer.py index b348a6e46..7a5322857 100644 --- a/codeflash/languages/function_optimizer.py +++ b/codeflash/languages/function_optimizer.py @@ -3085,6 +3085,16 @@ def run_optimized_candidate( ) ) + def get_js_project_root(self) -> Path | None: + # Only calculate for JavaScript/TypeScript projects + if self.function_to_optimize.language not in ("javascript", "typescript"): + return self.test_cfg.js_project_root # Fall back to cached value for non-JS + + # For JS/TS, calculate fresh for each function to support monorepos + from codeflash.languages.javascript.test_runner import find_node_project_root + + return find_node_project_root(Path(self.function_to_optimize.file_path)) + def run_and_parse_tests( self, testing_type: TestingMode, @@ -3103,33 +3113,39 @@ def run_and_parse_tests( coverage_config_file = None try: if testing_type == TestingMode.BEHAVIOR: + # Calculate js_project_root for the current function being optimized + # instead of using cached value from test_cfg, which may be from a different function + js_project_root = self.get_js_project_root() + result_file_path, run_result, coverage_database_file, coverage_config_file = ( self.language_support.run_behavioral_tests( test_paths=test_files, test_env=test_env, cwd=self.project_root, timeout=INDIVIDUAL_TESTCASE_TIMEOUT, - project_root=self.test_cfg.js_project_root, + project_root=js_project_root, enable_coverage=enable_coverage, candidate_index=optimization_iteration, ) ) elif testing_type == TestingMode.LINE_PROFILE: + js_project_root = self.get_js_project_root() result_file_path, run_result = self.language_support.run_line_profile_tests( test_paths=test_files, test_env=test_env, cwd=self.project_root, timeout=INDIVIDUAL_TESTCASE_TIMEOUT, - project_root=self.test_cfg.js_project_root, + project_root=js_project_root, line_profile_output_file=line_profiler_output_file, ) elif testing_type == TestingMode.PERFORMANCE: + js_project_root = self.get_js_project_root() result_file_path, run_result = self.language_support.run_benchmarking_tests( test_paths=test_files, test_env=test_env, cwd=self.project_root, timeout=INDIVIDUAL_TESTCASE_TIMEOUT, - project_root=self.test_cfg.js_project_root, + project_root=js_project_root, min_loops=pytest_min_loops, max_loops=pytest_max_loops, target_duration_seconds=testing_time, diff --git a/tests/test_js_project_root_per_function.py b/tests/test_js_project_root_per_function.py new file mode 100644 index 000000000..771b011a9 --- /dev/null +++ b/tests/test_js_project_root_per_function.py @@ -0,0 +1,66 @@ +"""Test that js_project_root is recalculated per function, not cached.""" + +from pathlib import Path + +from codeflash.languages.javascript.test_runner import find_node_project_root + + +def test_find_node_project_root_returns_different_roots_for_different_files(tmp_path: Path) -> None: + """Test that find_node_project_root returns the correct root for each file.""" + # Create main project structure + main_project = (tmp_path / "project").resolve() + main_project.mkdir() + (main_project / "package.json").write_text("{}", encoding="utf-8") + (main_project / "src").mkdir() + main_file = (main_project / "src" / "main.ts").resolve() + main_file.write_text("// main file", encoding="utf-8") + + # Create extension subdirectory with its own package.json + extension_dir = (main_project / "extensions" / "discord").resolve() + extension_dir.mkdir(parents=True) + (extension_dir / "package.json").write_text("{}", encoding="utf-8") + (extension_dir / "src").mkdir() + extension_file = (extension_dir / "src" / "accounts.ts").resolve() + extension_file.write_text("// extension file", encoding="utf-8") + + # Extension file should return extension directory + result1 = find_node_project_root(extension_file) + assert result1 == extension_dir, f"Expected {extension_dir}, got {result1}" + + # Main file should return main project directory + result2 = find_node_project_root(main_file) + assert result2 == main_project, f"Expected {main_project}, got {result2}" + + # Calling again with extension file should still return extension dir + result3 = find_node_project_root(extension_file) + assert result3 == extension_dir, f"Expected {extension_dir}, got {result3}" + + +def test_js_project_root_recalculated_per_function(tmp_path: Path) -> None: + """Each function in a monorepo should resolve to its own nearest package.json root.""" + # Create main project + main_project = (tmp_path / "project").resolve() + main_project.mkdir() + (main_project / "package.json").write_text('{"name": "main"}', encoding="utf-8") + (main_project / "src").mkdir() + + # Create extension with its own package.json + extension_dir = (main_project / "extensions" / "discord").resolve() + extension_dir.mkdir(parents=True) + (extension_dir / "package.json").write_text('{"name": "discord-extension"}', encoding="utf-8") + (extension_dir / "src").mkdir() + + extension_file = (extension_dir / "src" / "accounts.ts").resolve() + extension_file.write_text("export function foo() {}", encoding="utf-8") + + main_file = (main_project / "src" / "commands.ts").resolve() + main_file.write_text("export function bar() {}", encoding="utf-8") + + js_project_root_1 = find_node_project_root(extension_file) + assert js_project_root_1 == extension_dir + + js_project_root_2 = find_node_project_root(main_file) + assert js_project_root_2 == main_project, ( + f"Expected {main_project}, got {js_project_root_2}. " + f"Happens when js_project_root is not recalculated per function." + ) diff --git a/tests/test_optimizer_js_project_root_bug.py b/tests/test_optimizer_js_project_root_bug.py new file mode 100644 index 000000000..65e0237cb --- /dev/null +++ b/tests/test_optimizer_js_project_root_bug.py @@ -0,0 +1,57 @@ +"""Test that test_cfg.js_project_root caching bug is demonstrated and bypassed by the fix.""" + +from pathlib import Path +from unittest.mock import patch + +from codeflash.languages.javascript.support import JavaScriptSupport +from codeflash.verification.verification_utils import TestConfig + + +@patch("codeflash.languages.javascript.optimizer.verify_js_requirements") +def test_js_project_root_cached_in_test_cfg(mock_verify: object, tmp_path: Path) -> None: + """Demonstrates that test_cfg.js_project_root is set once per setup_test_config call. + + This test shows the root cause: test_cfg caches the project root from the first function. + The fix bypasses this cache in FunctionOptimizer.get_js_project_root() instead of + changing how test_cfg stores the value. + """ + mock_verify.return_value = [] # type: ignore[attr-defined] + + # Create main project + main_project = (tmp_path / "project").resolve() + main_project.mkdir() + (main_project / "package.json").write_text('{"name": "main"}', encoding="utf-8") + (main_project / "src").mkdir() + (main_project / "test").mkdir() + (main_project / "node_modules").mkdir() + + # Create extension with its own package.json + extension_dir = (main_project / "extensions" / "discord").resolve() + extension_dir.mkdir(parents=True) + (extension_dir / "package.json").write_text('{"name": "discord-extension"}', encoding="utf-8") + (extension_dir / "src").mkdir() + (extension_dir / "node_modules").mkdir() + + test_cfg = TestConfig( + tests_root=main_project / "test", + project_root_path=main_project, + tests_project_rootdir=main_project / "test", + ) + test_cfg.set_language("javascript") + + js_support = JavaScriptSupport() + + extension_file = (extension_dir / "src" / "accounts.ts").resolve() + extension_file.write_text("export function foo() {}", encoding="utf-8") + + success = js_support.setup_test_config(test_cfg, extension_file, current_worktree=None) + assert success, "setup_test_config should succeed" + # After setup for extension file, js_project_root is the extension directory + assert test_cfg.js_project_root == extension_dir + + # test_cfg is NOT re-initialized for subsequent functions — js_project_root stays cached + main_file = (main_project / "src" / "commands.ts").resolve() + main_file.write_text("export function bar() {}", encoding="utf-8") + + # The cached value is still extension_dir, not main_project — this is the root cause + assert test_cfg.js_project_root == extension_dir