Skip to content

Commit bd7185f

Browse files
Merge branch 'main' of github.com:codeflash-ai/codeflash into fix/skip-nested-functions-js
2 parents 432fad4 + fdf74e2 commit bd7185f

6 files changed

Lines changed: 257 additions & 9 deletions

File tree

codeflash/languages/javascript/edit_tests.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ def normalize_codeflash_imports(source: str) -> str:
226226
return _CODEFLASH_IMPORT_PATTERN.sub(r"import \1 from 'codeflash'", source)
227227

228228

229+
# Pattern to detect existing framework imports (regardless of specific identifiers imported)
230+
# This catches semantic duplicates even if the order/identifiers differ from what we'd inject
231+
_HAS_VITEST_IMPORT_RE = re.compile(r"import\s+\{[^}]*\}\s+from\s+['\"]vitest['\"]", re.MULTILINE)
232+
_HAS_JEST_IMPORT_RE = re.compile(r"import\s+\{[^}]*\}\s+from\s+['\"]@jest/globals['\"]", re.MULTILINE)
233+
_HAS_MOCHA_ASSERT_IMPORT_RE = re.compile(r"import\s+.*\s+from\s+['\"]node:assert", re.MULTILINE)
234+
_HAS_MOCHA_ASSERT_REQUIRE_RE = re.compile(r"(?:const|let|var)\s+.*\s*=\s*require\s*\(\s*['\"]node:assert", re.MULTILINE)
235+
236+
229237
# Author: ali <mohammed18200118@gmail.com>
230238
def inject_test_globals(
231239
generated_tests: GeneratedTestsList, test_framework: str = "jest", module_system: str = "esm"
@@ -246,24 +254,29 @@ def inject_test_globals(
246254
# Use vitest imports for vitest projects, jest imports for jest projects
247255
if test_framework == "vitest":
248256
global_import = "import { vi, describe, it, expect, beforeEach, afterEach, beforeAll, test } from 'vitest'\n"
257+
has_import_re = _HAS_VITEST_IMPORT_RE
249258
elif test_framework == "mocha":
250259
if is_cjs:
251260
global_import = "const assert = require('node:assert/strict');\n"
261+
has_import_re = _HAS_MOCHA_ASSERT_REQUIRE_RE
252262
else:
253263
global_import = "import assert from 'node:assert/strict';\n"
264+
has_import_re = _HAS_MOCHA_ASSERT_IMPORT_RE
254265
else:
255266
# Default to jest imports for jest and other frameworks
256267
global_import = (
257268
"import { jest, describe, it, expect, beforeEach, afterEach, beforeAll, test } from '@jest/globals'\n"
258269
)
270+
has_import_re = _HAS_JEST_IMPORT_RE
259271

260272
for test in generated_tests.generated_tests:
261-
# Skip injection if the source already has the import (LLM may have included it)
262-
if global_import.strip() not in test.generated_original_test_source:
273+
# Skip injection if the source already has ANY import from the framework
274+
# This catches semantic duplicates even if the AI used different identifiers/order
275+
if not has_import_re.search(test.generated_original_test_source):
263276
test.generated_original_test_source = global_import + test.generated_original_test_source
264-
if global_import.strip() not in test.instrumented_behavior_test_source:
277+
if not has_import_re.search(test.instrumented_behavior_test_source):
265278
test.instrumented_behavior_test_source = global_import + test.instrumented_behavior_test_source
266-
if global_import.strip() not in test.instrumented_perf_test_source:
279+
if not has_import_re.search(test.instrumented_perf_test_source):
267280
test.instrumented_perf_test_source = global_import + test.instrumented_perf_test_source
268281
return generated_tests
269282

codeflash/languages/javascript/module_system.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,54 @@ def ensure_vitest_imports(code: str, test_framework: str) -> str:
513513

514514
logger.debug("Added vitest imports: %s", used_globals)
515515
return "\n".join(lines)
516+
517+
518+
def add_js_extensions_to_relative_imports(code: str) -> str:
519+
"""Add .js extensions to relative imports in ESM code.
520+
521+
In ESM mode with TypeScript, Node.js requires explicit .js extensions
522+
for relative imports, even though the source files are .ts files.
523+
524+
This function adds .js extensions to relative imports that don't already
525+
have a file extension.
526+
527+
Args:
528+
code: JavaScript/TypeScript code with import statements.
529+
530+
Returns:
531+
Code with .js extensions added to relative imports.
532+
533+
Examples:
534+
>>> add_js_extensions_to_relative_imports("import X from './module';")
535+
"import X from './module.js';"
536+
537+
>>> add_js_extensions_to_relative_imports("import X from './module.js';")
538+
"import X from './module.js';"
539+
540+
>>> add_js_extensions_to_relative_imports("import X from 'node:assert';")
541+
"import X from 'node:assert';"
542+
543+
"""
544+
# Pattern to match ES module import statements with relative paths
545+
# Matches: import ... from './path' or import ... from "../path"
546+
# Groups: (import statement)(quote char)(relative path)(quote char)
547+
import_pattern = re.compile(
548+
r"(import\s+(?:(?:\{[^}]*\})|(?:\*\s+as\s+\w+)|(?:\w+))\s+from\s+)(['\"])(\.\.?[^'\"]+)(['\"])"
549+
)
550+
551+
def add_extension(match):
552+
"""Add .js extension if the import path doesn't have one."""
553+
prefix = match.group(1) # "import ... from "
554+
quote_open = match.group(2) # ' or "
555+
path = match.group(3) # The relative path (e.g., "./module" or "../foo/bar")
556+
quote_close = match.group(4) # ' or "
557+
558+
# Check if path already has an extension
559+
# Common extensions: .js, .ts, .jsx, .tsx, .mjs, .mts, .json
560+
if re.search(r"\.(js|ts|jsx|tsx|mjs|mts|json)$", path):
561+
return match.group(0)
562+
563+
# Add .js extension
564+
return f"{prefix}{quote_open}{path}.js{quote_close}"
565+
566+
return import_pattern.sub(add_extension, code)

codeflash/languages/javascript/support.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,6 +2018,7 @@ def process_generated_test_strings(
20182018
validate_and_fix_import_style,
20192019
)
20202020
from codeflash.languages.javascript.module_system import (
2021+
ModuleSystem,
20212022
ensure_module_system_compatibility,
20222023
ensure_vitest_imports,
20232024
)
@@ -2042,6 +2043,13 @@ def process_generated_test_strings(
20422043
generated_test_source, project_module_system, test_cfg.tests_project_rootdir
20432044
)
20442045

2046+
# Add .js extensions to relative imports for ESM projects
2047+
# TypeScript + ESM requires explicit .js extensions even for .ts source files
2048+
if project_module_system == ModuleSystem.ES_MODULE:
2049+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
2050+
2051+
generated_test_source = add_js_extensions_to_relative_imports(generated_test_source)
2052+
20452053
# Ensure vitest imports are present when using vitest framework
20462054
generated_test_source = ensure_vitest_imports(generated_test_source, test_cfg.test_framework)
20472055

docs/getting-started/javascript-installation.mdx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,15 @@ bun add --dev codeflash
7171
</Tip>
7272

7373
<Info>
74-
**Codeflash also requires a Python installation** (3.9+) to run the CLI optimizer. Install the Python CLI globally:
74+
**One-time setup required.** The Codeflash optimizer runs on Python behind the scenes. After installing the npm package, run:
7575

7676
```bash
77-
pip install codeflash
78-
# or
79-
uv pip install codeflash
77+
npx codeflash setup
8078
```
8179

82-
The Python CLI orchestrates the optimization pipeline, while the npm package provides the JavaScript runtime (test runners, serialization, reporters).
80+
This automatically creates an isolated Python environment — no global installs or manual Python management needed. After setup, all Codeflash commands run through `npx codeflash` which uses the installed binary automatically.
8381
</Info>
82+
8483
</Step>
8584

8685
<Step title="Generate a Codeflash API Key">
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Test for inject_test_globals duplicate import bug.
2+
3+
This test reproduces the bug where AI-generated tests already have vitest imports,
4+
but inject_test_globals() adds them again because the string-based check doesn't
5+
catch semantic duplicates with different identifier orders.
6+
"""
7+
8+
import pytest
9+
from codeflash.languages.javascript.edit_tests import inject_test_globals
10+
from codeflash.models.models import GeneratedTests, GeneratedTestsList
11+
from pathlib import Path
12+
13+
14+
def test_inject_test_globals_skips_existing_vitest_imports() -> None:
15+
"""Test that inject_test_globals skips injection when vitest import already exists."""
16+
# AI service generated this test with vitest imports already present
17+
# (note: different order and identifiers than what inject_test_globals would add)
18+
ai_generated_test = """// vitest imports (REQUIRED for vitest - globals are NOT enabled by default)
19+
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
20+
// function import
21+
import { isWindowsDrivePath } from './infra/archive-path';
22+
23+
// unit tests
24+
describe('isWindowsDrivePath', () => {
25+
test('should return true for Windows drive paths', () => {
26+
expect(isWindowsDrivePath('C:\\\\')).toBe(true);
27+
});
28+
});
29+
"""
30+
31+
generated_tests = GeneratedTestsList(
32+
generated_tests=[
33+
GeneratedTests(
34+
generated_original_test_source=ai_generated_test,
35+
instrumented_behavior_test_source=ai_generated_test,
36+
instrumented_perf_test_source=ai_generated_test,
37+
behavior_file_path=Path("/tmp/test_isWindowsDrivePath.test.ts"),
38+
perf_file_path=Path("/tmp/test_isWindowsDrivePath_perf.test.ts"),
39+
)
40+
]
41+
)
42+
43+
# Call inject_test_globals for vitest + esm (this is what the CLI does)
44+
result = inject_test_globals(generated_tests, test_framework="vitest", module_system="esm")
45+
46+
# Check that the import was NOT duplicated
47+
result_source = result.generated_tests[0].generated_original_test_source
48+
49+
# Count how many times "from 'vitest'" appears
50+
import_count = result_source.count("from 'vitest'")
51+
52+
# Should be exactly 1 import, not 2
53+
assert import_count == 1, (
54+
f"Expected exactly 1 vitest import, but found {import_count}. "
55+
f"inject_test_globals() added a duplicate import when one already existed.\n"
56+
f"Result:\n{result_source[:500]}"
57+
)
58+
59+
# Also verify that we have the expected number of import statements
60+
# Count actual import statements, not comments containing the word "import"
61+
import_lines = [line for line in result_source.split('\n') if line.strip().startswith('import ')]
62+
assert len(import_lines) == 2, f"Should have 2 import statements (vitest + function), found {len(import_lines)}: {import_lines}"
63+
64+
65+
def test_inject_test_globals_adds_import_when_missing() -> None:
66+
"""Test that inject_test_globals DOES add import when it's truly missing."""
67+
# Test without any vitest imports
68+
test_without_imports = """// function import
69+
import { isWindowsDrivePath } from './infra/archive-path';
70+
71+
describe('isWindowsDrivePath', () => {
72+
test('should return true', () => {
73+
expect(isWindowsDrivePath('C:\\\\')).toBe(true);
74+
});
75+
});
76+
"""
77+
78+
generated_tests = GeneratedTestsList(
79+
generated_tests=[
80+
GeneratedTests(
81+
generated_original_test_source=test_without_imports,
82+
instrumented_behavior_test_source=test_without_imports,
83+
instrumented_perf_test_source=test_without_imports,
84+
behavior_file_path=Path("/tmp/test.test.ts"),
85+
perf_file_path=Path("/tmp/test_perf.test.ts"),
86+
)
87+
]
88+
)
89+
90+
result = inject_test_globals(generated_tests, test_framework="vitest", module_system="esm")
91+
result_source = result.generated_tests[0].generated_original_test_source
92+
93+
# Should have exactly 1 vitest import (the one we added)
94+
import_count = result_source.count("from 'vitest'")
95+
assert import_count == 1, f"Expected vitest import to be added, found {import_count}"
96+
97+
# Should be at the beginning of the file
98+
assert result_source.startswith("import { vi, describe, it, expect"), (
99+
"Vitest import should be added at the beginning"
100+
)

tests/test_languages/test_javascript_module_system.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,80 @@ def test_real_world_budibase_import(self):
284284
result = convert_commonjs_to_esm(code)
285285
expected = "import { queue, context, db as dbCore, cache, events } from '@budibase/backend-core';"
286286
assert result == expected
287+
288+
289+
class TestAddJsExtensionsToRelativeImports:
290+
"""Tests for adding .js extensions to relative imports in ESM mode."""
291+
292+
def test_add_js_extension_to_relative_import(self):
293+
"""Test adding .js extension to relative import without extension."""
294+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
295+
296+
code = "import TreeNode from '../../injector/topology-tree/tree-node';"
297+
result = add_js_extensions_to_relative_imports(code)
298+
expected = "import TreeNode from '../../injector/topology-tree/tree-node.js';"
299+
assert result == expected
300+
301+
def test_add_js_extension_to_single_dot_import(self):
302+
"""Test adding .js extension to same-directory import."""
303+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
304+
305+
code = "import { foo } from './module';"
306+
result = add_js_extensions_to_relative_imports(code)
307+
expected = "import { foo } from './module.js';"
308+
assert result == expected
309+
310+
def test_skip_imports_with_existing_extensions(self):
311+
"""Test that imports with extensions are left unchanged."""
312+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
313+
314+
code = "import TreeNode from '../../tree-node.js';"
315+
result = add_js_extensions_to_relative_imports(code)
316+
assert result == code
317+
318+
code2 = "import TreeNode from '../../tree-node.ts';"
319+
result2 = add_js_extensions_to_relative_imports(code2)
320+
assert result2 == code2
321+
322+
def test_skip_node_modules_imports(self):
323+
"""Test that node_modules imports are left unchanged."""
324+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
325+
326+
code = "import assert from 'node:assert/strict';"
327+
result = add_js_extensions_to_relative_imports(code)
328+
assert result == code
329+
330+
code2 = "import { describe } from 'mocha';"
331+
result2 = add_js_extensions_to_relative_imports(code2)
332+
assert result2 == code2
333+
334+
def test_multiple_imports(self):
335+
"""Test handling multiple imports in one code block."""
336+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
337+
338+
code = """import assert from 'node:assert/strict';
339+
import TreeNode from '../../injector/topology-tree/tree-node';
340+
import { helper } from './helper';"""
341+
result = add_js_extensions_to_relative_imports(code)
342+
expected = """import assert from 'node:assert/strict';
343+
import TreeNode from '../../injector/topology-tree/tree-node.js';
344+
import { helper } from './helper.js';"""
345+
assert result == expected
346+
347+
def test_named_imports(self):
348+
"""Test adding extensions to named imports."""
349+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
350+
351+
code = "import { foo, bar } from '../utils/helpers';"
352+
result = add_js_extensions_to_relative_imports(code)
353+
expected = "import { foo, bar } from '../utils/helpers.js';"
354+
assert result == expected
355+
356+
def test_namespace_imports(self):
357+
"""Test adding extensions to namespace imports."""
358+
from codeflash.languages.javascript.module_system import add_js_extensions_to_relative_imports
359+
360+
code = "import * as helpers from '../utils';"
361+
result = add_js_extensions_to_relative_imports(code)
362+
expected = "import * as helpers from '../utils.js';"
363+
assert result == expected

0 commit comments

Comments
 (0)