Skip to content

Commit 08b9fe8

Browse files
Merge branch 'main' into fix/vitest-coverage-override
2 parents 1432413 + 973ebc2 commit 08b9fe8

9 files changed

Lines changed: 394 additions & 12 deletions

codeflash/languages/function_optimizer.py

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

3088+
def get_js_project_root(self) -> Path | None:
3089+
# Only calculate for JavaScript/TypeScript projects
3090+
if self.function_to_optimize.language not in ("javascript", "typescript"):
3091+
return self.test_cfg.js_project_root # Fall back to cached value for non-JS
3092+
3093+
# For JS/TS, calculate fresh for each function to support monorepos
3094+
from codeflash.languages.javascript.test_runner import find_node_project_root
3095+
3096+
return find_node_project_root(Path(self.function_to_optimize.file_path))
3097+
30883098
def run_and_parse_tests(
30893099
self,
30903100
testing_type: TestingMode,
@@ -3103,33 +3113,39 @@ def run_and_parse_tests(
31033113
coverage_config_file = None
31043114
try:
31053115
if testing_type == TestingMode.BEHAVIOR:
3116+
# Calculate js_project_root for the current function being optimized
3117+
# instead of using cached value from test_cfg, which may be from a different function
3118+
js_project_root = self.get_js_project_root()
3119+
31063120
result_file_path, run_result, coverage_database_file, coverage_config_file = (
31073121
self.language_support.run_behavioral_tests(
31083122
test_paths=test_files,
31093123
test_env=test_env,
31103124
cwd=self.project_root,
31113125
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3112-
project_root=self.test_cfg.js_project_root,
3126+
project_root=js_project_root,
31133127
enable_coverage=enable_coverage,
31143128
candidate_index=optimization_iteration,
31153129
)
31163130
)
31173131
elif testing_type == TestingMode.LINE_PROFILE:
3132+
js_project_root = self.get_js_project_root()
31183133
result_file_path, run_result = self.language_support.run_line_profile_tests(
31193134
test_paths=test_files,
31203135
test_env=test_env,
31213136
cwd=self.project_root,
31223137
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3123-
project_root=self.test_cfg.js_project_root,
3138+
project_root=js_project_root,
31243139
line_profile_output_file=line_profiler_output_file,
31253140
)
31263141
elif testing_type == TestingMode.PERFORMANCE:
3142+
js_project_root = self.get_js_project_root()
31273143
result_file_path, run_result = self.language_support.run_benchmarking_tests(
31283144
test_paths=test_files,
31293145
test_env=test_env,
31303146
cwd=self.project_root,
31313147
timeout=INDIVIDUAL_TESTCASE_TIMEOUT,
3132-
project_root=self.test_cfg.js_project_root,
3148+
project_root=js_project_root,
31333149
min_loops=pytest_min_loops,
31343150
max_loops=pytest_max_loops,
31353151
target_duration_seconds=testing_time,

codeflash/languages/javascript/instrument.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,13 +1287,13 @@ def fix_imports_inside_test_blocks(test_code: str) -> str:
12871287

12881288

12891289
def fix_jest_mock_paths(test_code: str, test_file_path: Path, source_file_path: Path, tests_root: Path) -> str:
1290-
"""Fix relative paths in jest.mock() calls to be correct from the test file's location.
1290+
"""Fix relative paths in jest.mock() and vi.mock() calls to be correct from the test file's location.
12911291
1292-
The AI sometimes generates jest.mock() calls with paths relative to the source file
1292+
The AI sometimes generates mock calls with paths relative to the source file
12931293
instead of the test file. For example:
12941294
- Source at `src/queue/queue.ts` imports `../environment` (-> src/environment)
1295-
- Test at `tests/test.test.ts` generates `jest.mock('../environment')` (-> ./environment, wrong!)
1296-
- Should generate `jest.mock('../src/environment')`
1295+
- Test at `tests/test.test.ts` generates `jest.mock('../environment')` or `vi.mock('../environment')` (-> ./environment, wrong!)
1296+
- Should generate `jest.mock('../src/environment')` or `vi.mock('../src/environment')`
12971297
12981298
This function detects relative mock paths and adjusts them based on the test file's
12991299
location relative to the source file's directory.
@@ -1318,8 +1318,8 @@ def fix_jest_mock_paths(test_code: str, test_file_path: Path, source_file_path:
13181318
test_dir = test_file_path.resolve().parent
13191319
project_root = tests_root.resolve().parent if tests_root.name == "tests" else tests_root.resolve()
13201320

1321-
# Pattern to match jest.mock() or jest.doMock() with relative paths
1322-
mock_pattern = re.compile(r"(jest\.(?:mock|doMock)\s*\(\s*['\"])(\.\./[^'\"]+|\.\/[^'\"]+)(['\"])")
1321+
# Pattern to match jest.mock(), jest.doMock(), or vi.mock() with relative paths
1322+
mock_pattern = re.compile(r"((?:jest|vi)\.(?:mock|doMock)\s*\(\s*['\"])(\.\./[^'\"]+|\.\/[^'\"]+)(['\"])")
13231323

13241324
def fix_mock_path(match: re.Match[str]) -> str:
13251325
original = match.group(0)
@@ -1359,7 +1359,7 @@ def fix_mock_path(match: re.Match[str]) -> str:
13591359
if not new_rel_path.startswith("../") and not new_rel_path.startswith("./"):
13601360
new_rel_path = f"./{new_rel_path}"
13611361

1362-
logger.debug(f"Fixed jest.mock path: {rel_path} -> {new_rel_path}")
1362+
logger.debug(f"Fixed mock path: {rel_path} -> {new_rel_path}")
13631363
return f"{prefix}{new_rel_path}{suffix}"
13641364

13651365
except (ValueError, OSError):

codeflash/languages/javascript/vitest_runner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import os
10+
import re
1011
import subprocess
1112
import time
1213
from pathlib import Path
@@ -169,7 +170,7 @@ def _is_vitest_workspace(project_root: Path) -> bool:
169170
return False
170171

171172
try:
172-
content = vitest_config.read_text()
173+
content = vitest_config.read_text(encoding="utf-8")
173174
# Check for actual workspace configuration patterns (not just the word "workspace" in comments)
174175
# Valid indicators:
175176
# - defineWorkspace() function call

codeflash/verification/verifier.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ def generate_tests(
3434
# TODO: Sometimes this recreates the original Class definition. This overrides and messes up the original
3535
# class import. Remove the recreation of the class definition
3636
start_time = time.perf_counter()
37-
test_module_path = Path(module_name_from_file_path(test_path, test_cfg.tests_project_rootdir))
37+
# Use traverse_up=True to handle co-located __tests__ directories that may be outside
38+
# the configured tests_root (e.g., src/gateway/__tests__/ when tests_root is test/)
39+
test_module_path = Path(module_name_from_file_path(test_path, test_cfg.tests_project_rootdir, traverse_up=True))
3840

3941
# Detect module system via language support (non-None for JS/TS, None for Python)
4042
lang_support = current_language_support()
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from codeflash.languages.javascript.vitest_runner import _ensure_codeflash_vitest_config
6+
7+
8+
def test_codeflash_vitest_config_overrides_setupfiles(tmp_path: Path) -> None:
9+
project_root = tmp_path.resolve()
10+
11+
# Create a project with setup file
12+
(project_root / "test").mkdir()
13+
(project_root / "test" / "setup.ts").write_text("// Setup file\n", encoding="utf-8")
14+
15+
vitest_config = """import { defineConfig } from 'vitest/config';
16+
17+
export default defineConfig({
18+
test: {
19+
setupFiles: ["test/setup.ts"], // Relative path - will cause issues
20+
include: ["src/**/*.test.ts"],
21+
},
22+
});
23+
"""
24+
(project_root / "vitest.config.ts").write_text(vitest_config, encoding="utf-8")
25+
26+
codeflash_config_path = _ensure_codeflash_vitest_config(project_root)
27+
28+
assert codeflash_config_path is not None
29+
assert codeflash_config_path.exists()
30+
31+
config_content = codeflash_config_path.read_text(encoding="utf-8")
32+
33+
assert "setupFiles" in config_content, (
34+
"Generated config must explicitly handle setupFiles to prevent "
35+
"relative path resolution issues. Current config:\n" + config_content
36+
)
37+
assert "setupFiles: []" in config_content or "setupFiles:" in config_content, (
38+
"setupFiles must be explicitly set in the merged config"
39+
)
40+
41+
42+
def test_codeflash_vitest_config_without_setupfiles(tmp_path: Path) -> None:
43+
project_root = tmp_path.resolve()
44+
45+
vitest_config = """import { defineConfig } from 'vitest/config';
46+
47+
export default defineConfig({
48+
test: {
49+
include: ["src/**/*.test.ts"],
50+
},
51+
});
52+
"""
53+
(project_root / "vitest.config.ts").write_text(vitest_config, encoding="utf-8")
54+
55+
codeflash_config_path = _ensure_codeflash_vitest_config(project_root)
56+
57+
assert codeflash_config_path is not None
58+
assert codeflash_config_path.exists()
59+
60+
config_content = codeflash_config_path.read_text(encoding="utf-8")
61+
assert "mergeConfig" in config_content or "defineConfig" in config_content
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Test fix_jest_mock_paths function with vitest mocks."""
2+
3+
from pathlib import Path
4+
5+
from codeflash.languages.javascript.instrument import fix_jest_mock_paths
6+
7+
8+
def test_fix_vitest_mock_paths():
9+
"""Test that vi.mock() paths are fixed correctly."""
10+
# Simulate source at src/agents/workspace.ts importing from ../routing/session-key
11+
# Test at test/test_workspace.test.ts should mock ../src/routing/session-key, not ../routing/session-key
12+
13+
test_code = """
14+
vi.mock('../routing/session-key', () => ({
15+
isSubagentSessionKey: vi.fn(),
16+
isCronSessionKey: vi.fn(),
17+
}));
18+
19+
import { filterBootstrapFilesForSession } from '../src/agents/workspace.js';
20+
"""
21+
22+
# Create temp directories and files for testing
23+
import tempfile
24+
25+
with tempfile.TemporaryDirectory() as tmpdir:
26+
project = Path(tmpdir)
27+
28+
# Create directory structure
29+
src = project / "src"
30+
src_agents = src / "agents"
31+
src_routing = src / "routing"
32+
test_dir = project / "test"
33+
34+
src_agents.mkdir(parents=True)
35+
src_routing.mkdir(parents=True)
36+
test_dir.mkdir(parents=True)
37+
38+
# Create files
39+
source_file = src_agents / "workspace.ts"
40+
source_file.write_text("export function filterBootstrapFilesForSession() {}")
41+
42+
routing_file = src_routing / "session-key.ts"
43+
routing_file.write_text("export function isSubagentSessionKey() {}")
44+
45+
test_file = test_dir / "test_workspace.test.ts"
46+
test_file.write_text(test_code)
47+
48+
# Fix the paths
49+
fixed = fix_jest_mock_paths(test_code, test_file, source_file, test_dir)
50+
51+
# Should change ../routing/session-key to ../src/routing/session-key
52+
assert "../src/routing/session-key" in fixed, f"Expected path to be fixed, got: {fixed}"
53+
assert "../routing/session-key" not in fixed or "../src/routing/session-key" in fixed
54+
55+
56+
def test_fix_jest_mock_paths_still_works():
57+
"""Test that jest.mock() paths are still fixed correctly."""
58+
test_code = """
59+
jest.mock('../routing/session-key', () => ({
60+
isSubagentSessionKey: jest.fn(),
61+
}));
62+
"""
63+
64+
import tempfile
65+
66+
with tempfile.TemporaryDirectory() as tmpdir:
67+
project = Path(tmpdir)
68+
src = project / "src"
69+
src_agents = src / "agents"
70+
src_routing = src / "routing"
71+
test_dir = project / "test"
72+
73+
src_agents.mkdir(parents=True)
74+
src_routing.mkdir(parents=True)
75+
test_dir.mkdir(parents=True)
76+
77+
source_file = src_agents / "workspace.ts"
78+
source_file.write_text("")
79+
80+
routing_file = src_routing / "session-key.ts"
81+
routing_file.write_text("")
82+
83+
test_file = test_dir / "test_workspace.test.ts"
84+
test_file.write_text(test_code)
85+
86+
fixed = fix_jest_mock_paths(test_code, test_file, source_file, test_dir)
87+
88+
assert "../src/routing/session-key" in fixed
89+
90+
91+
if __name__ == "__main__":
92+
test_fix_vitest_mock_paths()
93+
test_fix_jest_mock_paths_still_works()
94+
print("All tests passed!")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Test that js_project_root is recalculated per function, not cached."""
2+
3+
from pathlib import Path
4+
5+
from codeflash.languages.javascript.test_runner import find_node_project_root
6+
7+
8+
def test_find_node_project_root_returns_different_roots_for_different_files(tmp_path: Path) -> None:
9+
"""Test that find_node_project_root returns the correct root for each file."""
10+
# Create main project structure
11+
main_project = (tmp_path / "project").resolve()
12+
main_project.mkdir()
13+
(main_project / "package.json").write_text("{}", encoding="utf-8")
14+
(main_project / "src").mkdir()
15+
main_file = (main_project / "src" / "main.ts").resolve()
16+
main_file.write_text("// main file", encoding="utf-8")
17+
18+
# Create extension subdirectory with its own package.json
19+
extension_dir = (main_project / "extensions" / "discord").resolve()
20+
extension_dir.mkdir(parents=True)
21+
(extension_dir / "package.json").write_text("{}", encoding="utf-8")
22+
(extension_dir / "src").mkdir()
23+
extension_file = (extension_dir / "src" / "accounts.ts").resolve()
24+
extension_file.write_text("// extension file", encoding="utf-8")
25+
26+
# Extension file should return extension directory
27+
result1 = find_node_project_root(extension_file)
28+
assert result1 == extension_dir, f"Expected {extension_dir}, got {result1}"
29+
30+
# Main file should return main project directory
31+
result2 = find_node_project_root(main_file)
32+
assert result2 == main_project, f"Expected {main_project}, got {result2}"
33+
34+
# Calling again with extension file should still return extension dir
35+
result3 = find_node_project_root(extension_file)
36+
assert result3 == extension_dir, f"Expected {extension_dir}, got {result3}"
37+
38+
39+
def test_js_project_root_recalculated_per_function(tmp_path: Path) -> None:
40+
"""Each function in a monorepo should resolve to its own nearest package.json root."""
41+
# Create main project
42+
main_project = (tmp_path / "project").resolve()
43+
main_project.mkdir()
44+
(main_project / "package.json").write_text('{"name": "main"}', encoding="utf-8")
45+
(main_project / "src").mkdir()
46+
47+
# Create extension with its own package.json
48+
extension_dir = (main_project / "extensions" / "discord").resolve()
49+
extension_dir.mkdir(parents=True)
50+
(extension_dir / "package.json").write_text('{"name": "discord-extension"}', encoding="utf-8")
51+
(extension_dir / "src").mkdir()
52+
53+
extension_file = (extension_dir / "src" / "accounts.ts").resolve()
54+
extension_file.write_text("export function foo() {}", encoding="utf-8")
55+
56+
main_file = (main_project / "src" / "commands.ts").resolve()
57+
main_file.write_text("export function bar() {}", encoding="utf-8")
58+
59+
js_project_root_1 = find_node_project_root(extension_file)
60+
assert js_project_root_1 == extension_dir
61+
62+
js_project_root_2 = find_node_project_root(main_file)
63+
assert js_project_root_2 == main_project, (
64+
f"Expected {main_project}, got {js_project_root_2}. "
65+
f"Happens when js_project_root is not recalculated per function."
66+
)

0 commit comments

Comments
 (0)