Skip to content

Commit 9084d65

Browse files
Fix JS CI regressions from CLI follow-up
1 parent 1490afd commit 9084d65

8 files changed

Lines changed: 128 additions & 33 deletions

File tree

codeflash/code_utils/formatter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def apply_formatter_cmds(
6060
for command in cmds:
6161
formatter_cmd_list = shlex.split(command, posix=os.name != "nt")
6262
formatter_cmd_list = [file_path.as_posix() if chunk == file_token else chunk for chunk in formatter_cmd_list]
63+
if str(lang_support.language) in ("javascript", "typescript"):
64+
from codeflash.languages.javascript.command_utils import resolve_node_command_list
65+
66+
formatter_cmd_list = resolve_node_command_list(formatter_cmd_list)
6367
try:
6468
result = subprocess.run(formatter_cmd_list, capture_output=True, check=False)
6569
if result.returncode == 0:

codeflash/discovery/functions_to_optimize.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import ast
4-
import contextlib
54
import os
65
import random
76
import warnings

codeflash/verification/verifier.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ def generate_tests(
6161

6262
source_file_abs = source_file.resolve().with_suffix("")
6363
test_dir_abs = test_path.resolve().parent
64-
# Compute relative path from test directory to source file
65-
rel_import_path = os.path.relpath(str(source_file_abs), str(test_dir_abs))
64+
# Compute relative path from test directory to source file.
65+
# JavaScript import specifiers must always use forward slashes, even on Windows.
66+
rel_import_path = os.path.relpath(str(source_file_abs), str(test_dir_abs)).replace("\\", "/")
6667
# Ensure path starts with ./ or ../ for JavaScript/TypeScript imports
67-
if not rel_import_path.startswith("../"):
68+
if not rel_import_path.startswith(("../", "./")):
6869
rel_import_path = f"./{rel_import_path}"
6970
# ESM requires explicit file extensions in import specifiers.
7071
# TypeScript ESM also uses .js extensions (TS resolves .js → .ts).

tests/languages/javascript/test_vitest_runner.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
)
1717

1818

19+
def command_name(command: str) -> str:
20+
return Path(command).stem.lower()
21+
22+
1923
class TestFindVitestProjectRoot:
2024
"""Tests for _find_vitest_project_root function."""
2125

@@ -96,9 +100,8 @@ def test_basic_command_structure(self) -> None:
96100

97101
cmd = _build_vitest_behavioral_command([test_file], timeout=60)
98102

99-
assert cmd[0] == "npx"
100-
assert cmd[1] == "vitest"
101-
assert cmd[2] == "run"
103+
assert command_name(cmd[0]) == "npx"
104+
assert cmd[1:3] == ["vitest", "run"]
102105

103106
def test_includes_reporter_flags(self) -> None:
104107
"""Should include reporter flags for JUnit output."""
@@ -174,9 +177,8 @@ def test_basic_command_structure(self) -> None:
174177

175178
cmd = _build_vitest_benchmarking_command([test_file], timeout=60)
176179

177-
assert cmd[0] == "npx"
178-
assert cmd[1] == "vitest"
179-
assert cmd[2] == "run"
180+
assert command_name(cmd[0]) == "npx"
181+
assert cmd[1:3] == ["vitest", "run"]
180182

181183
def test_includes_serial_execution(self) -> None:
182184
"""Should include serial execution for consistent benchmarking."""
@@ -202,7 +204,8 @@ def test_vitest_uses_run_subcommand(self) -> None:
202204

203205
vitest_cmd = _build_vitest_behavioral_command([test_file], timeout=60)
204206

205-
assert vitest_cmd[0:3] == ["npx", "vitest", "run"]
207+
assert command_name(vitest_cmd[0]) == "npx"
208+
assert vitest_cmd[1:3] == ["vitest", "run"]
206209

207210
def test_vitest_uses_hyphenated_timeout(self) -> None:
208211
"""Vitest uses --test-timeout, Jest uses --testTimeout (camelCase)."""

tests/test_formatter.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
import pytest
77

88
from codeflash.code_utils.config_parser import parse_config_file
9-
from codeflash.code_utils.formatter import format_code, format_generated_code, sort_imports
9+
from codeflash.code_utils.formatter import apply_formatter_cmds, format_code, format_generated_code, sort_imports
1010
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
11-
from codeflash.models.models import CodeString, CodeStringsMarkdown
1211
from codeflash.languages.function_optimizer import FunctionOptimizer
12+
from codeflash.models.models import CodeString, CodeStringsMarkdown
1313
from codeflash.verification.verification_utils import TestConfig
1414

1515

@@ -1394,6 +1394,30 @@ def test_format_generated_code_unicode():
13941394
assert "Hello, 世界! 🌍" in result
13951395

13961396

1397+
def test_apply_formatter_cmds_resolves_node_wrappers_for_javascript(tmp_path: Path):
1398+
"""JavaScript formatter commands should resolve npx/npm wrappers before subprocess execution."""
1399+
from unittest.mock import MagicMock, patch
1400+
1401+
js_file = tmp_path / "test.js"
1402+
js_file.write_text("const value = 1;\n", encoding="utf-8")
1403+
resolved_cmd = [r"C:\nvm4w\nodejs\npx.cmd", "prettier", "--write", js_file.as_posix()]
1404+
1405+
with (
1406+
patch(
1407+
"codeflash.languages.javascript.command_utils.resolve_node_command_list", return_value=resolved_cmd
1408+
) as mock_resolve,
1409+
patch("codeflash.code_utils.formatter.subprocess.run", return_value=MagicMock(returncode=0)) as mock_run,
1410+
):
1411+
_, formatted_code, changed = apply_formatter_cmds(
1412+
["npx prettier --write $file"], js_file, test_dir_str=None, print_status=False
1413+
)
1414+
1415+
mock_resolve.assert_called_once_with(["npx", "prettier", "--write", js_file.as_posix()])
1416+
mock_run.assert_called_once_with(resolved_cmd, capture_output=True, check=False)
1417+
assert changed is True
1418+
assert formatted_code == js_file.read_text(encoding="utf-8")
1419+
1420+
13971421
def test_format_generated_code_uses_correct_extension_for_javascript():
13981422
"""Test that format_generated_code creates temp files with .js extension for JavaScript code."""
13991423
from unittest.mock import patch

tests/test_init_javascript.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for JavaScript/TypeScript project initialization and package manager detection."""
22

33
import json
4+
import tempfile
45
from pathlib import Path
56
from unittest.mock import patch
67

@@ -17,9 +18,15 @@
1718

1819

1920
@pytest.fixture
20-
def tmp_project(tmp_path: Path) -> Path:
21-
"""Create a temporary project directory."""
22-
return tmp_path
21+
def tmp_project() -> Path:
22+
"""Create a temporary project directory with a deterministic parent chain."""
23+
with tempfile.TemporaryDirectory(dir=Path.cwd()) as tmp_dir:
24+
yield Path(tmp_dir)
25+
26+
27+
def assert_install_command(actual: list[str], executable: str, expected_args: list[str]) -> None:
28+
assert Path(actual[0]).stem.lower() == executable
29+
assert actual[1:] == expected_args
2330

2431

2532
class TestDetermineJsPackageManager:
@@ -206,7 +213,7 @@ def test_npm_install_command(self, tmp_project: Path) -> None:
206213

207214
result = get_package_install_command(tmp_project, "codeflash", dev=True)
208215

209-
assert result == ["npm", "install", "codeflash", "--save-dev"]
216+
assert_install_command(result, "npm", ["install", "codeflash", "--save-dev"])
210217

211218
def test_npm_install_command_non_dev(self, tmp_project: Path) -> None:
212219
"""Should return npm install command without --save-dev when dev=False."""
@@ -215,7 +222,7 @@ def test_npm_install_command_non_dev(self, tmp_project: Path) -> None:
215222

216223
result = get_package_install_command(tmp_project, "codeflash", dev=False)
217224

218-
assert result == ["npm", "install", "codeflash"]
225+
assert_install_command(result, "npm", ["install", "codeflash"])
219226

220227
def test_pnpm_add_command(self, tmp_project: Path) -> None:
221228
"""Should return pnpm add command for pnpm projects."""
@@ -224,7 +231,7 @@ def test_pnpm_add_command(self, tmp_project: Path) -> None:
224231

225232
result = get_package_install_command(tmp_project, "codeflash", dev=True)
226233

227-
assert result == ["pnpm", "add", "codeflash", "--save-dev"]
234+
assert_install_command(result, "pnpm", ["add", "codeflash", "--save-dev"])
228235

229236
def test_pnpm_add_command_non_dev(self, tmp_project: Path) -> None:
230237
"""Should return pnpm add command without --save-dev when dev=False."""
@@ -233,7 +240,7 @@ def test_pnpm_add_command_non_dev(self, tmp_project: Path) -> None:
233240

234241
result = get_package_install_command(tmp_project, "codeflash", dev=False)
235242

236-
assert result == ["pnpm", "add", "codeflash"]
243+
assert_install_command(result, "pnpm", ["add", "codeflash"])
237244

238245
def test_yarn_add_command(self, tmp_project: Path) -> None:
239246
"""Should return yarn add command for yarn projects."""
@@ -242,7 +249,7 @@ def test_yarn_add_command(self, tmp_project: Path) -> None:
242249

243250
result = get_package_install_command(tmp_project, "codeflash", dev=True)
244251

245-
assert result == ["yarn", "add", "codeflash", "--dev"]
252+
assert_install_command(result, "yarn", ["add", "codeflash", "--dev"])
246253

247254
def test_yarn_add_command_non_dev(self, tmp_project: Path) -> None:
248255
"""Should return yarn add command without --dev when dev=False."""
@@ -251,7 +258,7 @@ def test_yarn_add_command_non_dev(self, tmp_project: Path) -> None:
251258

252259
result = get_package_install_command(tmp_project, "codeflash", dev=False)
253260

254-
assert result == ["yarn", "add", "codeflash"]
261+
assert_install_command(result, "yarn", ["add", "codeflash"])
255262

256263
def test_bun_add_command(self, tmp_project: Path) -> None:
257264
"""Should return bun add command for bun projects."""
@@ -260,7 +267,7 @@ def test_bun_add_command(self, tmp_project: Path) -> None:
260267

261268
result = get_package_install_command(tmp_project, "codeflash", dev=True)
262269

263-
assert result == ["bun", "add", "codeflash", "--dev"]
270+
assert_install_command(result, "bun", ["add", "codeflash", "--dev"])
264271

265272
def test_bun_add_command_non_dev(self, tmp_project: Path) -> None:
266273
"""Should return bun add command without --dev when dev=False."""
@@ -269,14 +276,14 @@ def test_bun_add_command_non_dev(self, tmp_project: Path) -> None:
269276

270277
result = get_package_install_command(tmp_project, "codeflash", dev=False)
271278

272-
assert result == ["bun", "add", "codeflash"]
279+
assert_install_command(result, "bun", ["add", "codeflash"])
273280

274281
def test_defaults_to_npm_for_unknown(self, tmp_project: Path) -> None:
275282
"""Should default to npm for unknown package manager."""
276283
# No lockfile, no package.json - unknown package manager
277284
result = get_package_install_command(tmp_project, "codeflash", dev=True)
278285

279-
assert result == ["npm", "install", "codeflash", "--save-dev"]
286+
assert_install_command(result, "npm", ["install", "codeflash", "--save-dev"])
280287

281288
def test_different_package_name(self, tmp_project: Path) -> None:
282289
"""Should work with different package names."""
@@ -285,7 +292,7 @@ def test_different_package_name(self, tmp_project: Path) -> None:
285292

286293
result = get_package_install_command(tmp_project, "typescript", dev=True)
287294

288-
assert result == ["pnpm", "add", "typescript", "--save-dev"]
295+
assert_install_command(result, "pnpm", ["add", "typescript", "--save-dev"])
289296

290297

291298
class TestShouldModifySkipConfirm:

tests/test_languages/test_javascript_requirements.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
import json
7-
from pathlib import Path
7+
from pathlib import Path, PureWindowsPath
88
from unittest.mock import MagicMock, patch
99

1010
import pytest
@@ -15,6 +15,12 @@
1515
class TestVerifyRequirements:
1616
"""Tests for JavaScriptSupport.verify_requirements()."""
1717

18+
@staticmethod
19+
def _command_name(command: str) -> str:
20+
if "\\" in command:
21+
return PureWindowsPath(command).stem.lower()
22+
return Path(command).stem.lower()
23+
1824
@pytest.fixture
1925
def js_support(self):
2026
"""Create a JavaScriptSupport instance."""
@@ -98,7 +104,7 @@ def test_verify_requirements_fails_without_npm(self, js_support, project_with_je
98104
"""Test verification fails when npm is not available."""
99105

100106
def mock_run_side_effect(cmd, **kwargs):
101-
command_name = Path(cmd[0]).stem.lower()
107+
command_name = self._command_name(cmd[0])
102108
if command_name == "node":
103109
return MagicMock(returncode=0)
104110
if command_name == "npm":
@@ -113,13 +119,10 @@ def mock_run_side_effect(cmd, **kwargs):
113119
assert npm_error_found is True
114120

115121
def test_verify_requirements_accepts_windows_cmd_wrappers(self, js_support, project_with_jest):
116-
resolved_commands = {
117-
"node": r"C:\nvm4w\nodejs\node.exe",
118-
"npm": r"C:\nvm4w\nodejs\npm.cmd",
119-
}
122+
resolved_commands = {"node": r"C:\nvm4w\nodejs\node.exe", "npm": r"C:\nvm4w\nodejs\npm.cmd"}
120123

121124
def mock_run_side_effect(cmd, **kwargs):
122-
command_name = Path(cmd[0]).stem.lower()
125+
command_name = self._command_name(cmd[0])
123126
assert cmd[0] == resolved_commands[command_name]
124127
return MagicMock(returncode=0)
125128

tests/verification/test_verifier_path_handling.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
"""
99

1010
from pathlib import Path
11+
from unittest.mock import MagicMock, patch
1112

1213
import pytest
1314

1415
from codeflash.code_utils.code_utils import module_name_from_file_path
16+
from codeflash.models.function_types import FunctionToOptimize
17+
from codeflash.verification.verifier import generate_tests
1518

1619

1720
class TestVerifierPathHandling:
@@ -53,3 +56,54 @@ def test_module_name_from_file_path_with_fallback_succeeds(self) -> None:
5356

5457
# After fallback, we should have a valid path
5558
assert test_module_path == "test_foo.test.ts"
59+
60+
def test_generate_tests_uses_forward_slashes_for_javascript_module_paths(self, tmp_path: Path) -> None:
61+
"""Generated JS import paths should stay valid on Windows by using forward slashes."""
62+
project_root = tmp_path / "project"
63+
source_dir = project_root / "src"
64+
source_dir.mkdir(parents=True)
65+
66+
source_file = source_dir / "async_utils.js"
67+
source_file.write_text("export async function processItemsSequential() {}", encoding="utf-8")
68+
69+
generated_tests_dir = source_dir / "__tests__" / "codeflash-generated"
70+
generated_tests_dir.mkdir(parents=True)
71+
test_path = generated_tests_dir / "test_processItemsSequential__unit_test_0.test.js"
72+
test_perf_path = generated_tests_dir / "test_processItemsSequential__perf_test_0.test.js"
73+
74+
function_to_optimize = FunctionToOptimize(
75+
function_name="processItemsSequential", file_path=source_file, language="javascript"
76+
)
77+
test_cfg = MagicMock(tests_project_rootdir=project_root / "tests", test_framework="jest")
78+
ai_client = MagicMock()
79+
ai_client.generate_regression_tests.return_value = ("generated", "behavior", "perf", None)
80+
81+
mock_support = MagicMock()
82+
mock_support.detect_module_system.return_value = "esm"
83+
mock_support.language_version = None
84+
mock_support.process_generated_test_strings.side_effect = lambda **kwargs: (
85+
kwargs["generated_test_source"],
86+
kwargs["instrumented_behavior_test_source"],
87+
kwargs["instrumented_perf_test_source"],
88+
)
89+
90+
with patch("codeflash.verification.verifier.current_language_support", return_value=mock_support):
91+
result = generate_tests(
92+
aiservice_client=ai_client,
93+
source_code_being_tested=source_file.read_text(encoding="utf-8"),
94+
function_to_optimize=function_to_optimize,
95+
helper_function_names=[],
96+
module_path=source_file,
97+
test_cfg=test_cfg,
98+
test_timeout=30,
99+
function_trace_id="trace-id",
100+
test_index=0,
101+
test_path=test_path,
102+
test_perf_path=test_perf_path,
103+
)
104+
105+
assert result is not None
106+
module_path = ai_client.generate_regression_tests.call_args.kwargs["module_path"]
107+
assert module_path == "../../async_utils.js"
108+
assert "\\" not in module_path
109+
assert not module_path.startswith("./..")

0 commit comments

Comments
 (0)