Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions codeflash/languages/function_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions tests/test_js_project_root_per_function.py
Original file line number Diff line number Diff line change
@@ -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."
)
57 changes: 57 additions & 0 deletions tests/test_optimizer_js_project_root_bug.py
Original file line number Diff line number Diff line change
@@ -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
Loading