Fix: Strip .js extensions from vi.mock() calls in Vitest tests (#2524)

## Summary

Vitest tests were failing with "Cannot find module" errors because
`vi.mock()` calls retained `.js` extensions while imports had them
stripped, causing mock/import path mismatch in ESM mode.

## Root Cause

The `strip_js_extensions()` function in `testgen.py` only handled
`jest.mock()` but not `vi.mock()`, which is used by Vitest. The pattern
`_JEST_MOCK_EXTENSION_PATTERN` matched Jest mocking functions but not
Vitest's `vi.*` equivalents.

## Fix

Added `_VITEST_MOCK_EXTENSION_PATTERN` regex to match and strip
extensions from:
- `vi.mock()`
- `vi.doMock()`
- `vi.unmock()`
- `vi.requireActual()`
- `vi.requireMock()`
- `vi.importActual()`
- `vi.importMock()`

## Affected Trace IDs

- `0fe99c9f-b348-4f0a-b051-0ea9455231ba`
- `127cdaec-a343-4918-a86a-b646dd4d79cf`
- `2b6c896e-20d7-4505-8bf4-e4a2f20b37fc`

These trace IDs exhibited the bug where generated tests had
`vi.mock('../config/paths.js')` but imports had `from
'../config/paths'`, causing module resolution failures.

## Test Coverage

- Added 8 new tests in `TestStripJsExtensions` class
- All 31 tests in `test_testgen_javascript.py` pass
- Specific regression test for vi.mock() extension stripping
- Tests cover all vi.mock variants and edge cases

## Files Changed

- `django/aiservice/core/languages/js_ts/testgen.py` (fix)
- `django/aiservice/tests/testgen/test_testgen_javascript.py` (tests)

---------

Co-authored-by: Codeflash Bot <codeflash-bot@codeflash.ai>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
This commit is contained in:
mohammed ahmed 2026-04-02 18:20:45 +02:00 committed by GitHub
parent 179302d006
commit de0f30ae15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 3 deletions

View file

@ -141,12 +141,15 @@ _REQUIRE_EXTENSION_PATTERN = re.compile(
_JEST_MOCK_EXTENSION_PATTERN = re.compile(
r"""(jest\.(?:mock|doMock|unmock|requireActual|requireMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
)
_VITEST_MOCK_EXTENSION_PATTERN = re.compile(
r"""(vi\.(?:mock|doMock|unmock|requireActual|requireMock|importActual|importMock)\s*\(\s*['"])(\.{0,2}/[^'"]+?)(\.(?:js|ts|tsx|jsx|mjs|mts))(['"])"""
)
def strip_js_extensions(source: str) -> str:
"""Strip .js/.ts/.tsx/.jsx extensions from relative import paths.
TypeScript and Jest's module resolution automatically resolve file extensions,
TypeScript and Jest/Vitest module resolution automatically resolve file extensions,
so adding them explicitly can cause "Cannot find module" errors when the LLM
adds incorrect extensions (e.g., .js to a .ts file).
@ -154,6 +157,7 @@ def strip_js_extensions(source: str) -> str:
- ES module imports: import { x } from '../path/file.js'
- CommonJS requires: require('../path/file.js')
- Jest mocks: jest.mock('../path/file.js')
- Vitest mocks: vi.mock('../path/file.js')
Args:
source: The test source code.
@ -164,7 +168,8 @@ def strip_js_extensions(source: str) -> str:
"""
source = _JS_EXTENSION_PATTERN.sub(r"\1\2\4", source)
source = _REQUIRE_EXTENSION_PATTERN.sub(r"\1\2\4", source)
return _JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
source = _JEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
return _VITEST_MOCK_EXTENSION_PATTERN.sub(r"\1\2\4", source)
def _detect_export_style(source_code: str, identifier: str) -> str | None:

View file

@ -7,7 +7,7 @@ import re
import pytest
from core.languages.js_ts.testgen import build_javascript_prompt, parse_and_validate_js_output
from core.languages.js_ts.testgen import build_javascript_prompt, parse_and_validate_js_output, strip_js_extensions
def _has_test_functions(code: str) -> bool:
@ -343,3 +343,93 @@ class TestMochaPromptContent:
assert isinstance(user_content, str)
assert "vitest" in user_content.lower()
assert "expect" in user_content.lower()
class TestStripJsExtensions:
"""Tests for stripping .js/.ts extensions from import paths."""
def test_strips_extensions_from_imports(self) -> None:
"""Test that extensions are stripped from ES module imports."""
source = "import { x } from '../path/file.js';"
result = strip_js_extensions(source)
assert result == "import { x } from '../path/file';"
def test_strips_extensions_from_require(self) -> None:
"""Test that extensions are stripped from require() calls."""
source = "const x = require('../path/file.js');"
result = strip_js_extensions(source)
assert result == "const x = require('../path/file');"
def test_strips_extensions_from_jest_mock(self) -> None:
"""Test that extensions are stripped from jest.mock() calls."""
source = "jest.mock('../path/file.js', () => {});"
result = strip_js_extensions(source)
assert result == "jest.mock('../path/file', () => {});"
def test_strips_extensions_from_vi_mock(self) -> None:
"""Test that extensions are stripped from vi.mock() calls (Vitest).
This is a regression test for the bug where vi.mock() paths retained
.js extensions while imports had them stripped, causing mock/import
path mismatch in Vitest ESM mode.
Trace IDs affected:
- 0fe99c9f-b348-4f0a-b051-0ea9455231ba
- 127cdaec-a343-4918-a86a-b646dd4d79cf
- 2b6c896e-20d7-4505-8bf4-e4a2f20b37fc
"""
source = "vi.mock('../config/paths.js', () => {});"
result = strip_js_extensions(source)
# This test will FAIL until the bug is fixed
assert result == "vi.mock('../config/paths', () => {});"
def test_strips_extensions_from_complex_vi_mock(self) -> None:
"""Test extension stripping for complex vi.mock() with multiline callback."""
source = """vi.mock('../config/paths.js', () => {
return {
resolveCredentialsDir: vi.fn(() => '/mock/credentials'),
};
});"""
result = strip_js_extensions(source)
assert "vi.mock('../config/paths'" in result
assert "vi.mock('../config/paths.js'" not in result
def test_strips_all_vi_mock_variants(self) -> None:
"""Test that all vi.mock variants are handled."""
source = """
vi.mock('../a.js', () => {});
vi.doMock('../b.js', () => {});
vi.unmock('../c.js');
"""
result = strip_js_extensions(source)
assert "../a'" in result
assert "../b'" in result
assert "../c'" in result
assert ".js" not in result
def test_preserves_node_modules_paths(self) -> None:
"""Test that node_modules paths (without ./) are not modified."""
source = "import { x } from 'some-package';"
result = strip_js_extensions(source)
assert result == source
def test_handles_mixed_mocks_and_imports(self) -> None:
"""Test realistic scenario with both vi.mock() and imports."""
source = """vi.mock('../config/paths.js', () => {
return {
resolveCredentialsDir: vi.fn(() => '/mock/credentials'),
};
});
import { resolveChannelAllowFromPath } from './pairing/pairing-store.js';
import { resolveCredentialsDir } from '../config/paths.js';"""
result = strip_js_extensions(source)
# All .js extensions should be removed
assert "vi.mock('../config/paths'" in result
assert "from './pairing/pairing-store'" in result
assert "from '../config/paths'" in result
# No .js should remain
assert ".js" not in result