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>
This commit is contained in:
mohammed ahmed 2026-04-03 14:30:24 +00:00
parent a756701bb2
commit 46c49910cd
3 changed files with 276 additions and 3 deletions

View file

@ -3085,6 +3085,30 @@ 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.
"""
# 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
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)
def run_and_parse_tests(
self,
testing_type: TestingMode,
@ -3103,33 +3127,39 @@ class FunctionOptimizer:
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,

View file

@ -0,0 +1,95 @@
"""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():
"""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 = 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"
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")
# 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}"
)
# 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}"
)
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.
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 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()
# 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()
# Files to optimize
extension_file = extension_dir / "src" / "accounts.ts"
extension_file.write_text("export function foo() {}")
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."
)

View file

@ -0,0 +1,148 @@
"""
Test for the bug where test_cfg.js_project_root is set once and reused.
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 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):
"""
This test demonstrates the bug where js_project_root is set once
and never updated when optimizing functions from different directories.
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 = []
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
# 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
# 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
# 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")
# Create JavaScript support instance
js_support = JavaScriptSupport()
# Optimize function 1 (in extension directory)
extension_file = extension_dir / "src" / "accounts.ts"
extension_file.write_text("export function foo() {}")
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