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
4 changes: 2 additions & 2 deletions codeflash/cli_cmds/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ def process_pyproject_config(args: Namespace) -> Namespace:
is_js_ts_project = pyproject_config.get("language") in ("javascript", "typescript")

# Set the test framework singleton for JS/TS projects
if is_js_ts_project and pyproject_config.get("test_runner"):
set_current_test_framework(pyproject_config["test_runner"])
if is_js_ts_project and pyproject_config.get("test_framework"):
set_current_test_framework(pyproject_config["test_framework"])

if args.tests_root is None:
if is_js_ts_project:
Expand Down
17 changes: 12 additions & 5 deletions codeflash/code_utils/config_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,18 @@ def parse_package_json_config(package_json_path: Path) -> tuple[dict[str, Any],

Most configuration is auto-detected from package.json and project structure.
Only minimal config is stored in the "codeflash" key:
- moduleRoot: Override auto-detected module root (optional)
- testsRoot: Override auto-detected tests root (optional)
- test-framework: Override auto-detected test framework - "jest", "vitest", or "mocha" (optional)
- benchmarksRoot: Where to store benchmark files (optional, defaults to __benchmarks__)
- ignorePaths: Paths to exclude from optimization (optional)
- disableTelemetry: Privacy preference (optional, defaults to false)
- formatterCmds: Override auto-detected formatter (optional)

Auto-detected values (not stored in config):
Auto-detected values (used when not explicitly configured):
- language: Detected from tsconfig.json presence
- moduleRoot: Detected from package.json exports/module/main or src/ convention
- testRunner: Detected from devDependencies (vitest/jest/mocha)
- test-framework: Detected from devDependencies (vitest/jest/mocha)
- formatter: Detected from devDependencies (prettier/eslint)

Args:
Expand Down Expand Up @@ -254,10 +257,14 @@ def parse_package_json_config(package_json_path: Path) -> tuple[dict[str, Any],
if codeflash_config.get("testsRoot"):
config["tests_root"] = str(project_root / Path(codeflash_config["testsRoot"]).resolve())

# Auto-detect test runner
config["test_runner"] = detect_test_runner(project_root, package_data)
# Check for explicit test framework override, otherwise auto-detect
# Uses "test-framework" to match Python's pyproject.toml convention
if codeflash_config.get("test-framework"):
config["test_framework"] = codeflash_config["test-framework"]
else:
config["test_framework"] = detect_test_runner(project_root, package_data)
# Keep pytest_cmd for backwards compatibility with existing code
config["pytest_cmd"] = config["test_runner"]
config["pytest_cmd"] = config["test_framework"]

# Auto-detect formatter (with optional override from config)
if "formatterCmds" in codeflash_config:
Expand Down
4 changes: 2 additions & 2 deletions codeflash/setup/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class CodeflashConfig(BaseModel):
tests_root: str | None = Field(default=None, description="Root directory containing tests")

# Tooling settings (auto-detected, can be overridden)
test_runner: str | None = Field(default=None, description="Test runner: pytest, jest, vitest, mocha")
test_framework: str | None = Field(default=None, description="Test framework: pytest, jest, vitest, mocha")
formatter_cmds: list[str] = Field(default_factory=list, description="Formatter commands")

# Optional settings
Expand Down Expand Up @@ -145,7 +145,7 @@ def from_detected_project(cls, detected: Any) -> CodeflashConfig:
if detected.module_root != detected.project_root
else ".",
tests_root=str(detected.tests_root.relative_to(detected.project_root)) if detected.tests_root else None,
test_runner=detected.test_runner,
test_framework=detected.test_runner,
formatter_cmds=detected.formatter_cmds,
ignore_paths=[
str(p.relative_to(detected.project_root)) for p in detected.ignore_paths if p != detected.project_root
Expand Down
150 changes: 145 additions & 5 deletions tests/code_utils/test_config_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ def test_parses_minimal_package_json(self, tmp_path: Path) -> None:
assert result is not None
config, path = result
assert config["language"] == "javascript"
assert config["test_runner"] == "jest"
assert config["test_framework"] == "jest"
assert config["pytest_cmd"] == "jest"
assert path == package_json

Expand Down Expand Up @@ -728,7 +728,7 @@ def test_nextjs_project(self, tmp_path: Path) -> None:
config, _ = result
assert config["language"] == "typescript"
assert config["module_root"] == str((tmp_path / "src").resolve())
assert config["test_runner"] == "jest"
assert config["test_framework"] == "jest"
assert config["formatter_cmds"] == ["npx prettier --write $file"]

def test_vite_react_project(self, tmp_path: Path) -> None:
Expand All @@ -752,7 +752,7 @@ def test_vite_react_project(self, tmp_path: Path) -> None:
assert result is not None
config, _ = result
assert config["language"] == "typescript"
assert config["test_runner"] == "vitest"
assert config["test_framework"] == "vitest"
assert config["formatter_cmds"] == ["npx eslint --fix $file"]

def test_library_with_exports(self, tmp_path: Path) -> None:
Expand Down Expand Up @@ -812,7 +812,7 @@ def test_node_cli_project(self, tmp_path: Path) -> None:
assert result is not None
config, _ = result
assert config["module_root"] == str((tmp_path / "lib").resolve())
assert config["test_runner"] == "mocha"
assert config["test_framework"] == "mocha"

def test_minimal_project(self, tmp_path: Path) -> None:
"""Should handle minimal package.json."""
Expand All @@ -825,7 +825,7 @@ def test_minimal_project(self, tmp_path: Path) -> None:
config, _ = result
assert config["language"] == "javascript"
assert config["module_root"] == str(tmp_path.resolve())
assert config["test_runner"] == "jest"
assert config["test_framework"] == "jest"
assert config["formatter_cmds"] == []

def test_existing_codeflash_config_with_overrides(self, tmp_path: Path) -> None:
Expand Down Expand Up @@ -855,3 +855,143 @@ def test_existing_codeflash_config_with_overrides(self, tmp_path: Path) -> None:
assert config["formatter_cmds"] == ["npx prettier --write --single-quote $file"]
assert len(config["ignore_paths"]) == 2
assert config["disable_telemetry"] is True


class TestTestFrameworkConfigOverride:
"""Tests for explicit test-framework config override (matches Python's pyproject.toml)."""

def test_test_framework_overrides_auto_detection(self, tmp_path: Path) -> None:
"""Should use test-framework from codeflash config instead of auto-detecting from devDependencies."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"vitest": "^1.0.0"},
"codeflash": {"test-framework": "jest"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "jest"
assert config["pytest_cmd"] == "jest"

def test_explicit_vitest_config_with_jest_in_deps(self, tmp_path: Path) -> None:
"""Should use explicit vitest config even when jest is in devDependencies."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"jest": "^29.0.0", "vitest": "^1.0.0"},
"codeflash": {"test-framework": "vitest"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "vitest"

def test_explicit_mocha_overrides_vitest_and_jest(self, tmp_path: Path) -> None:
"""Should use explicit mocha config even when vitest and jest are in devDependencies."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"vitest": "^1.0.0", "jest": "^29.0.0"},
"codeflash": {"test-framework": "mocha"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "mocha"

def test_auto_detection_when_no_explicit_config(self, tmp_path: Path) -> None:
"""Should auto-detect test framework when no explicit config is provided."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"vitest": "^1.0.0"},
"codeflash": {"moduleRoot": "src"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "vitest"

def test_empty_test_framework_falls_back_to_auto_detection(self, tmp_path: Path) -> None:
"""Should auto-detect when test-framework is empty string."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"jest": "^29.0.0"},
"codeflash": {"test-framework": ""},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "jest"

def test_custom_test_framework_value(self, tmp_path: Path) -> None:
"""Should accept custom test framework values not in the standard list."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"vitest": "^1.0.0"},
"codeflash": {"test-framework": "ava"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "ava"

def test_pytest_cmd_matches_test_framework_with_override(self, tmp_path: Path) -> None:
"""Should set pytest_cmd to match test_framework when using explicit config."""
package_json = tmp_path / "package.json"
package_json.write_text(
json.dumps(
{
"name": "test-project",
"devDependencies": {"vitest": "^1.0.0"},
"codeflash": {"test-framework": "jest"},
}
)
)

result = parse_package_json_config(package_json)

assert result is not None
config, _ = result
assert config["test_framework"] == "jest"
assert config["pytest_cmd"] == "jest"
assert config["test_framework"] == config["pytest_cmd"]
6 changes: 3 additions & 3 deletions tests/test_setup/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_all_fields(self):
language="javascript",
module_root="src",
tests_root="tests",
test_runner="jest",
test_framework="jest",
formatter_cmds=["npx prettier --write $file"],
ignore_paths=["dist", "node_modules"],
benchmarks_root="benchmarks",
Expand All @@ -46,7 +46,7 @@ def test_all_fields(self):
assert config.language == "javascript"
assert config.module_root == "src"
assert config.tests_root == "tests"
assert config.test_runner == "jest"
assert config.test_framework == "jest"
assert config.formatter_cmds == ["npx prettier --write $file"]
assert config.ignore_paths == ["dist", "node_modules"]
assert config.git_remote == "upstream"
Expand Down Expand Up @@ -128,7 +128,7 @@ def test_from_detected_project(self, tmp_path):
config = CodeflashConfig.from_detected_project(detected)

assert config.language == detected.language
assert config.test_runner == detected.test_runner
assert config.test_framework == detected.test_runner

def test_from_pyproject_dict(self):
"""Should create config from pyproject.toml dict."""
Expand Down
Loading