diff --git a/codeflash/languages/function_optimizer.py b/codeflash/languages/function_optimizer.py index b462f86c5..7a5322857 100644 --- a/codeflash/languages/function_optimizer.py +++ b/codeflash/languages/function_optimizer.py @@ -3085,29 +3085,15 @@ class FunctionOptimizer: ) ) - def _get_js_project_root(self) -> Path | None: - """Get the JavaScript project root for the current function being optimized. - - This method calculates the js_project_root for each function instead of - caching it in test_cfg. This is important in monorepos where different - functions may belong to different packages/extensions with their own - package.json files. - - Returns: - Path to the JavaScript project root, or None if not a JavaScript project - or if the project root cannot be determined. - """ + 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 - from pathlib import Path - + # For JS/TS, calculate fresh for each function to support monorepos from codeflash.languages.javascript.test_runner import find_node_project_root - source_file = Path(self.function_to_optimize.file_path) - return find_node_project_root(source_file) + return find_node_project_root(Path(self.function_to_optimize.file_path)) def run_and_parse_tests( self, @@ -3129,7 +3115,7 @@ class FunctionOptimizer: 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() + 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( @@ -3143,7 +3129,7 @@ class FunctionOptimizer: ) ) elif testing_type == TestingMode.LINE_PROFILE: - js_project_root = self._get_js_project_root() + 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, @@ -3153,7 +3139,7 @@ class FunctionOptimizer: line_profile_output_file=line_profiler_output_file, ) elif testing_type == TestingMode.PERFORMANCE: - js_project_root = self._get_js_project_root() + 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, diff --git a/tests/test_js_project_root_per_function.py b/tests/test_js_project_root_per_function.py index 1b52a291c..771b011a9 100644 --- a/tests/test_js_project_root_per_function.py +++ b/tests/test_js_project_root_per_function.py @@ -1,95 +1,66 @@ """Test that js_project_root is recalculated per function, not cached.""" -import tempfile from pathlib import Path -import pytest - from codeflash.languages.javascript.test_runner import find_node_project_root -def test_find_node_project_root_returns_different_roots_for_different_files(): +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.""" - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) + # 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 main project structure - main_project = root / "project" - main_project.mkdir() - (main_project / "package.json").write_text("{}") - (main_project / "src").mkdir() - main_file = main_project / "src" / "main.ts" - main_file.write_text("// main file") + # 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") - # Create extension subdirectory with its own package.json - extension_dir = main_project / "extensions" / "discord" - extension_dir.mkdir(parents=True) - (extension_dir / "package.json").write_text("{}") - (extension_dir / "src").mkdir() - extension_file = extension_dir / "src" / "accounts.ts" - extension_file.write_text("// extension file") + # Extension file should return extension directory + result1 = find_node_project_root(extension_file) + assert result1 == extension_dir, f"Expected {extension_dir}, got {result1}" - # Test 1: 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}" - # Test 2: Main file should return main project directory - result2 = find_node_project_root(main_file) - assert result2 == main_project, ( - f"Expected {main_project}, got {result2}" - ) - - # Test 3: 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}" - ) + # 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_should_be_recalculated_per_function(): - """ - Test the actual bug: when optimizing multiple functions from different - directories, each should get its own js_project_root, not inherit from - the first function. +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() - This test simulates the scenario where: - 1. Function #1 is in extensions/discord/src/accounts.ts - 2. Function #2 is in src/plugins/commands.ts - 3. Both should get their correct respective project roots - """ - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) + # 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() - # Create main project - main_project = root / "project" - main_project.mkdir() - (main_project / "package.json").write_text('{"name": "main"}') - (main_project / "src").mkdir() - (main_project / "test").mkdir() + extension_file = (extension_dir / "src" / "accounts.ts").resolve() + extension_file.write_text("export function foo() {}", encoding="utf-8") - # Create extension with its own package.json - extension_dir = main_project / "extensions" / "discord" - extension_dir.mkdir(parents=True) - (extension_dir / "package.json").write_text('{"name": "discord-extension"}') - (extension_dir / "src").mkdir() + main_file = (main_project / "src" / "commands.ts").resolve() + main_file.write_text("export function bar() {}", encoding="utf-8") - # Files to optimize - extension_file = extension_dir / "src" / "accounts.ts" - extension_file.write_text("export function foo() {}") + js_project_root_1 = find_node_project_root(extension_file) + assert js_project_root_1 == extension_dir - main_file = main_project / "src" / "commands.ts" - main_file.write_text("export function bar() {}") - - # Simulate what happens in Codeflash optimizer - # Function 1 (extension file) sets js_project_root - js_project_root_1 = find_node_project_root(extension_file) - assert js_project_root_1 == extension_dir - - # Function 2 (main file) should get its own root, not inherit from function 1 - js_project_root_2 = find_node_project_root(main_file) - assert js_project_root_2 == main_project, ( - f"Bug reproduced: main file got {js_project_root_2} instead of {main_project}. " - f"This happens when test_cfg.js_project_root is not recalculated per function." - ) + 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 index 8fefb1657..65e0237cb 100644 --- a/tests/test_optimizer_js_project_root_bug.py +++ b/tests/test_optimizer_js_project_root_bug.py @@ -1,148 +1,57 @@ -""" -Test for the bug where test_cfg.js_project_root is set once and reused. +"""Test that test_cfg.js_project_root caching bug is demonstrated and bypassed by the fix.""" -The bug: When optimizing multiple functions from different directories in a monorepo, -the js_project_root from the FIRST function is cached in test_cfg and used for ALL -subsequent functions, causing incorrect vitest working directories. -""" - -import tempfile from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest +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_not_recalculated_demonstrates_bug(mock_verify): +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. """ - This test demonstrates the bug where js_project_root is set once - and never updated when optimizing functions from different directories. + mock_verify.return_value = [] # type: ignore[attr-defined] - Expected behavior: Each function should get its own js_project_root - Actual behavior: All functions share the first function's js_project_root - """ - # Mock verify_js_requirements to always pass - mock_verify.return_value = [] + # 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() - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) + # 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() - # Create main project - main_project = root / "project" - main_project.mkdir() - (main_project / "package.json").write_text('{"name": "main"}') - (main_project / "src").mkdir() - (main_project / "test").mkdir() - (main_project / "node_modules").mkdir() # Add node_modules to pass requirements check + test_cfg = TestConfig( + tests_root=main_project / "test", + project_root_path=main_project, + tests_project_rootdir=main_project / "test", + ) + test_cfg.set_language("javascript") - # Create extension with its own package.json - extension_dir = main_project / "extensions" / "discord" - extension_dir.mkdir(parents=True) - (extension_dir / "package.json").write_text('{"name": "discord-extension"}') - (extension_dir / "src").mkdir() - (extension_dir / "node_modules").mkdir() # Add node_modules to pass requirements check + js_support = JavaScriptSupport() - # Create test config (shared across all functions, simulating optimizer behavior) - test_cfg = TestConfig( - tests_root=main_project / "test", - project_root_path=main_project, - tests_project_rootdir=main_project / "test", - ) - test_cfg.set_language("javascript") + extension_file = (extension_dir / "src" / "accounts.ts").resolve() + extension_file.write_text("export function foo() {}", encoding="utf-8") - # Create JavaScript support instance - js_support = JavaScriptSupport() + 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 - # Optimize function 1 (in extension directory) - extension_file = extension_dir / "src" / "accounts.ts" - extension_file.write_text("export function foo() {}") + # 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") - success = js_support.setup_test_config(test_cfg, extension_file, current_worktree=None) - assert success, "setup_test_config should succeed" - js_project_root_after_func1 = test_cfg.js_project_root - - # Should be extension directory - assert js_project_root_after_func1 == extension_dir, ( - f"Function 1: Expected {extension_dir}, got {js_project_root_after_func1}" - ) - - # Optimize function 2 (in main src directory) - main_file = main_project / "src" / "commands.ts" - main_file.write_text("export function bar() {}") - - # This is the bug: setup_test_config is NOT called again in the real code! - # The test_cfg object is reused, so js_project_root stays as extension_dir - - # In the real optimizer, test_cfg is reused without calling setup_test_config again - # So js_project_root remains the same from function 1 - js_project_root_for_func2 = test_cfg.js_project_root - - # BUG: This assertion should fail because js_project_root was not recalculated - # It's still pointing to extension_dir instead of main_project - assert js_project_root_for_func2 == extension_dir, ( - f"BUG DEMONSTRATED: Function 2 inherits function 1's js_project_root. " - f"Expected {main_project}, got {js_project_root_for_func2}" - ) - - # What SHOULD happen: - # js_support.setup_test_config(test_cfg, main_file, current_worktree=None) - # correct_root = test_cfg.js_project_root - # assert correct_root == main_project - - -@pytest.mark.xfail(reason="Demonstrates the bug - will fail once bug is fixed") -@patch("codeflash.languages.javascript.optimizer.verify_js_requirements") -def test_js_project_root_reused_across_functions_wrong_behavior(mock_verify): - """ - This test is marked xfail because it currently PASSES (demonstrating the bug). - Once the bug is fixed, this test will FAIL (which is correct), and we can remove xfail. - """ - # Mock verify_js_requirements to always pass - mock_verify.return_value = [] - - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - - main_project = root / "project" - main_project.mkdir() - (main_project / "package.json").write_text('{"name": "main"}') - (main_project / "src").mkdir() - (main_project / "test").mkdir() - (main_project / "node_modules").mkdir() - - extension_dir = main_project / "extensions" / "discord" - extension_dir.mkdir(parents=True) - (extension_dir / "package.json").write_text('{"name": "discord"}') - (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() - - # Set up for extension file - extension_file = extension_dir / "src" / "accounts.ts" - extension_file.write_text("export function foo() {}") - js_support.setup_test_config(test_cfg, extension_file, current_worktree=None) - - # Now try to use test_cfg for a different file - main_file = main_project / "src" / "commands.ts" - main_file.write_text("export function bar() {}") - - # This assertion will PASS (showing the bug) because js_project_root is wrong - # Once fixed, this will FAIL because js_project_root will be recalculated - assert test_cfg.js_project_root == extension_dir, ( - "Bug exists: js_project_root is not recalculated per function" - ) - - # The correct behavior would be: - # assert test_cfg.js_project_root == main_project + # The cached value is still extension_dir, not main_project — this is the root cause + assert test_cfg.js_project_root == extension_dir