# Conflicts: # .claude/rules/architecture.md # .claude/rules/code-style.md # .github/workflows/claude.yml # .github/workflows/duplicate-code-detector.yml # codeflash/api/aiservice.py # codeflash/cli_cmds/console.py # codeflash/cli_cmds/logging_config.py # codeflash/code_utils/deduplicate_code.py # codeflash/discovery/discover_unit_tests.py # codeflash/languages/base.py # codeflash/languages/code_replacer.py # codeflash/languages/javascript/mocha_runner.py # codeflash/languages/javascript/support.py # codeflash/languages/python/support.py # codeflash/optimization/function_optimizer.py # codeflash/verification/parse_test_output.py # codeflash/verification/verification_utils.py # codeflash/verification/verifier.py # packages/codeflash/package-lock.json # packages/codeflash/package.json # tests/languages/javascript/test_support_dispatch.py # tests/test_codeflash_capture.py # tests/test_languages/test_javascript_test_runner.py # tests/test_multi_file_code_replacement.py
992 lines
36 KiB
Python
992 lines
36 KiB
Python
"""Tests for JavaScript/TypeScript configuration detection and parsing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from codeflash.code_utils.config_js import (
|
|
PACKAGE_JSON_CACHE,
|
|
PACKAGE_JSON_DATA_CACHE,
|
|
clear_cache,
|
|
detect_formatter,
|
|
detect_language,
|
|
detect_module_root,
|
|
detect_test_runner,
|
|
find_package_json,
|
|
get_package_json_data,
|
|
parse_package_json_config,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_caches() -> None:
|
|
"""Clear all caches before each test."""
|
|
clear_cache()
|
|
|
|
|
|
class TestGetPackageJsonData:
|
|
"""Tests for get_package_json_data function."""
|
|
|
|
def test_loads_valid_package_json(self, tmp_path: Path) -> None:
|
|
"""Should load and return valid package.json data."""
|
|
package_json = tmp_path / "package.json"
|
|
data = {"name": "test-project", "version": "1.0.0"}
|
|
package_json.write_text(json.dumps(data))
|
|
|
|
result = get_package_json_data(package_json)
|
|
|
|
assert result == data
|
|
|
|
def test_caches_loaded_data(self, tmp_path: Path) -> None:
|
|
"""Should cache package.json data after first load."""
|
|
package_json = tmp_path / "package.json"
|
|
data = {"name": "test-project"}
|
|
package_json.write_text(json.dumps(data))
|
|
|
|
# First call
|
|
result1 = get_package_json_data(package_json)
|
|
# Modify file
|
|
package_json.write_text(json.dumps({"name": "modified"}))
|
|
# Second call should return cached data
|
|
result2 = get_package_json_data(package_json)
|
|
|
|
assert result1 == result2 == data
|
|
|
|
def test_returns_none_for_invalid_json(self, tmp_path: Path) -> None:
|
|
"""Should return None for invalid JSON."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text("{ invalid json }")
|
|
|
|
result = get_package_json_data(package_json)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_for_nonexistent_file(self, tmp_path: Path) -> None:
|
|
"""Should return None for non-existent file."""
|
|
package_json = tmp_path / "package.json"
|
|
|
|
result = get_package_json_data(package_json)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="chmod doesn't restrict read access on Windows")
|
|
def test_returns_none_for_unreadable_file(self, tmp_path: Path) -> None:
|
|
"""Should return None if file cannot be read."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text("{}")
|
|
package_json.chmod(0o000)
|
|
|
|
try:
|
|
result = get_package_json_data(package_json)
|
|
assert result is None
|
|
finally:
|
|
package_json.chmod(0o644)
|
|
|
|
|
|
class TestDetectLanguage:
|
|
"""Tests for detect_language function."""
|
|
|
|
def test_detects_typescript_with_tsconfig(self, tmp_path: Path) -> None:
|
|
"""Should detect TypeScript when tsconfig.json exists."""
|
|
(tmp_path / "tsconfig.json").write_text("{}")
|
|
|
|
result = detect_language(tmp_path)
|
|
|
|
assert result == "typescript"
|
|
|
|
def test_detects_javascript_without_tsconfig(self, tmp_path: Path) -> None:
|
|
"""Should detect JavaScript when no tsconfig.json exists."""
|
|
result = detect_language(tmp_path)
|
|
|
|
assert result == "javascript"
|
|
|
|
def test_detects_typescript_with_complex_tsconfig(self, tmp_path: Path) -> None:
|
|
"""Should detect TypeScript even with complex tsconfig."""
|
|
tsconfig = {"compilerOptions": {"target": "ES2020", "module": "commonjs"}, "include": ["src/**/*"]}
|
|
(tmp_path / "tsconfig.json").write_text(json.dumps(tsconfig))
|
|
|
|
result = detect_language(tmp_path)
|
|
|
|
assert result == "typescript"
|
|
|
|
|
|
class TestDetectModuleRoot:
|
|
"""Tests for detect_module_root function."""
|
|
|
|
def test_detects_from_exports_string(self, tmp_path: Path) -> None:
|
|
"""Should detect module root from exports string field."""
|
|
(tmp_path / "lib").mkdir()
|
|
package_data = {"exports": "./lib/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "lib"
|
|
|
|
def test_detects_from_exports_object_dot(self, tmp_path: Path) -> None:
|
|
"""Should skip build output dirs and return '.' when no src dir exists."""
|
|
(tmp_path / "dist").mkdir()
|
|
package_data = {"exports": {".": "./dist/index.js"}}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
# dist is a build output directory, so it's skipped
|
|
assert result == "."
|
|
|
|
def test_detects_from_exports_object_nested(self, tmp_path: Path) -> None:
|
|
"""Should detect module root from nested exports object."""
|
|
(tmp_path / "src").mkdir()
|
|
package_data = {"exports": {".": {"import": "./src/index.mjs", "require": "./src/index.cjs"}}}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "src"
|
|
|
|
def test_detects_from_exports_import_key(self, tmp_path: Path) -> None:
|
|
"""Should detect from exports with direct import key."""
|
|
(tmp_path / "esm").mkdir()
|
|
package_data = {"exports": {"import": "./esm/index.js"}}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "esm"
|
|
|
|
def test_detects_from_module_field(self, tmp_path: Path) -> None:
|
|
"""Should detect module root from module field (ESM entry)."""
|
|
(tmp_path / "es").mkdir()
|
|
package_data = {"module": "./es/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "es"
|
|
|
|
def test_detects_from_main_field(self, tmp_path: Path) -> None:
|
|
"""Should detect module root from main field (CJS entry)."""
|
|
(tmp_path / "lib").mkdir()
|
|
package_data = {"main": "./lib/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "lib"
|
|
|
|
def test_prefers_exports_over_module(self, tmp_path: Path) -> None:
|
|
"""Should prefer exports field over module field."""
|
|
(tmp_path / "exports-dir").mkdir()
|
|
(tmp_path / "module-dir").mkdir()
|
|
package_data = {"exports": "./exports-dir/index.js", "module": "./module-dir/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "exports-dir"
|
|
|
|
def test_prefers_module_over_main(self, tmp_path: Path) -> None:
|
|
"""Should prefer module field over main field."""
|
|
(tmp_path / "esm").mkdir()
|
|
(tmp_path / "cjs").mkdir()
|
|
package_data = {"module": "./esm/index.js", "main": "./cjs/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "esm"
|
|
|
|
def test_detects_src_directory_convention(self, tmp_path: Path) -> None:
|
|
"""Should detect src/ directory when no package.json fields point elsewhere."""
|
|
(tmp_path / "src").mkdir()
|
|
package_data = {}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "src"
|
|
|
|
def test_falls_back_to_current_directory(self, tmp_path: Path) -> None:
|
|
"""Should fall back to '.' when nothing else matches."""
|
|
package_data = {}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "."
|
|
|
|
def test_ignores_nonexistent_directory_from_exports(self, tmp_path: Path) -> None:
|
|
"""Should ignore exports pointing to non-existent directory."""
|
|
(tmp_path / "src").mkdir()
|
|
package_data = {"exports": "./nonexistent/index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "src"
|
|
|
|
def test_ignores_root_level_main(self, tmp_path: Path) -> None:
|
|
"""Should ignore main that points to root level file."""
|
|
(tmp_path / "src").mkdir()
|
|
package_data = {"main": "./index.js"}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "src"
|
|
|
|
def test_handles_deeply_nested_exports(self, tmp_path: Path) -> None:
|
|
"""Should handle deeply nested export paths but skip build output dirs."""
|
|
(tmp_path / "packages" / "core" / "dist").mkdir(parents=True)
|
|
package_data = {"exports": {".": {"import": "./packages/core/dist/index.mjs"}}}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
# dist is a build output directory, so it's skipped even when nested
|
|
assert result == "."
|
|
|
|
def test_handles_empty_exports(self, tmp_path: Path) -> None:
|
|
"""Should handle empty exports gracefully."""
|
|
(tmp_path / "src").mkdir()
|
|
package_data = {"exports": {}}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "src"
|
|
|
|
def test_handles_null_exports(self, tmp_path: Path) -> None:
|
|
"""Should handle null/None exports gracefully."""
|
|
package_data = {"exports": None}
|
|
|
|
result = detect_module_root(tmp_path, package_data)
|
|
|
|
assert result == "."
|
|
|
|
|
|
class TestDetectTestRunner:
|
|
"""Tests for detect_test_runner function."""
|
|
|
|
def test_detects_vitest_from_dev_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should detect vitest from devDependencies."""
|
|
package_data = {"devDependencies": {"vitest": "^1.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "vitest"
|
|
|
|
def test_detects_jest_from_dev_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should detect jest from devDependencies."""
|
|
package_data = {"devDependencies": {"jest": "^29.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_detects_mocha_from_dev_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should detect mocha from devDependencies."""
|
|
package_data = {"devDependencies": {"mocha": "^10.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "mocha"
|
|
|
|
def test_detects_from_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should also check dependencies (not just devDependencies)."""
|
|
package_data = {"dependencies": {"jest": "^29.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_prefers_vitest_over_jest(self, tmp_path: Path) -> None:
|
|
"""Should prefer vitest when both are present."""
|
|
package_data = {"devDependencies": {"vitest": "^1.0.0", "jest": "^29.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "vitest"
|
|
|
|
def test_prefers_jest_over_mocha(self, tmp_path: Path) -> None:
|
|
"""Should prefer jest over mocha."""
|
|
package_data = {"devDependencies": {"jest": "^29.0.0", "mocha": "^10.0.0"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_detects_vitest_from_test_script(self, tmp_path: Path) -> None:
|
|
"""Should detect vitest from scripts.test."""
|
|
package_data = {"scripts": {"test": "vitest run"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "vitest"
|
|
|
|
def test_detects_jest_from_test_script(self, tmp_path: Path) -> None:
|
|
"""Should detect jest from scripts.test."""
|
|
package_data = {"scripts": {"test": "jest --coverage"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_detects_mocha_from_test_script(self, tmp_path: Path) -> None:
|
|
"""Should detect mocha from scripts.test."""
|
|
package_data = {"scripts": {"test": "mocha tests/**/*.js"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "mocha"
|
|
|
|
def test_detects_from_npx_command(self, tmp_path: Path) -> None:
|
|
"""Should detect runner from npx command in test script."""
|
|
package_data = {"scripts": {"test": "npx jest"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_detects_case_insensitive(self, tmp_path: Path) -> None:
|
|
"""Should detect runner case-insensitively from scripts."""
|
|
package_data = {"scripts": {"test": "JEST --ci"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_prefers_deps_over_scripts(self, tmp_path: Path) -> None:
|
|
"""Should prefer devDependencies detection over scripts."""
|
|
package_data = {"devDependencies": {"vitest": "^1.0.0"}, "scripts": {"test": "jest"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "vitest"
|
|
|
|
def test_defaults_to_jest(self, tmp_path: Path) -> None:
|
|
"""Should default to jest when nothing is detected."""
|
|
package_data = {}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_handles_complex_test_script(self, tmp_path: Path) -> None:
|
|
"""Should detect from complex test scripts."""
|
|
package_data = {"scripts": {"test": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage"}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_handles_missing_scripts(self, tmp_path: Path) -> None:
|
|
"""Should handle missing scripts gracefully."""
|
|
package_data = {"name": "test"}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
def test_handles_non_string_test_script(self, tmp_path: Path) -> None:
|
|
"""Should handle non-string test script gracefully."""
|
|
package_data = {"scripts": {"test": 123}}
|
|
|
|
result = detect_test_runner(tmp_path, package_data)
|
|
|
|
assert result == "jest"
|
|
|
|
|
|
class TestDetectFormatter:
|
|
"""Tests for detect_formatter function."""
|
|
|
|
def test_detects_prettier_from_dev_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should detect prettier from devDependencies."""
|
|
package_data = {"devDependencies": {"prettier": "^3.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result == ["npx prettier --write $file"]
|
|
|
|
def test_detects_eslint_from_dev_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should detect eslint from devDependencies."""
|
|
package_data = {"devDependencies": {"eslint": "^8.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result == ["npx eslint --fix $file"]
|
|
|
|
def test_detects_from_dependencies(self, tmp_path: Path) -> None:
|
|
"""Should also check dependencies."""
|
|
package_data = {"dependencies": {"prettier": "^3.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result == ["npx prettier --write $file"]
|
|
|
|
def test_prefers_prettier_over_eslint(self, tmp_path: Path) -> None:
|
|
"""Should prefer prettier when both are present."""
|
|
package_data = {"devDependencies": {"prettier": "^3.0.0", "eslint": "^8.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result == ["npx prettier --write $file"]
|
|
|
|
def test_returns_none_when_no_formatter(self, tmp_path: Path) -> None:
|
|
"""Should return None when no formatter is detected."""
|
|
package_data = {"devDependencies": {"typescript": "^5.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_for_empty_deps(self, tmp_path: Path) -> None:
|
|
"""Should return None for empty dependencies."""
|
|
package_data = {}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result is None
|
|
|
|
def test_detects_eslint_related_packages(self, tmp_path: Path) -> None:
|
|
"""Should detect eslint even with scoped packages."""
|
|
package_data = {"devDependencies": {"eslint": "^8.0.0", "@eslint/js": "^8.0.0"}}
|
|
|
|
result = detect_formatter(tmp_path, package_data)
|
|
|
|
assert result == ["npx eslint --fix $file"]
|
|
|
|
|
|
class TestFindPackageJson:
|
|
"""Tests for find_package_json function."""
|
|
|
|
def test_finds_explicit_package_json(self, tmp_path: Path) -> None:
|
|
"""Should find explicitly provided package.json path."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text("{}")
|
|
|
|
result = find_package_json(package_json)
|
|
|
|
assert result == package_json
|
|
|
|
def test_returns_none_for_wrong_filename(self, tmp_path: Path) -> None:
|
|
"""Should return None if explicit path is not package.json."""
|
|
other_file = tmp_path / "other.json"
|
|
other_file.write_text("{}")
|
|
|
|
result = find_package_json(other_file)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_for_nonexistent_explicit(self, tmp_path: Path) -> None:
|
|
"""Should return None if explicit package.json doesn't exist."""
|
|
package_json = tmp_path / "package.json"
|
|
|
|
result = find_package_json(package_json)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestParsePackageJsonConfig:
|
|
"""Tests for parse_package_json_config function."""
|
|
|
|
def test_parses_minimal_package_json(self, tmp_path: Path) -> None:
|
|
"""Should parse package.json without codeflash section."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "devDependencies": {"jest": "^29.0.0"}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, path = result
|
|
assert config["language"] == "javascript"
|
|
assert config["test_framework"] == "jest"
|
|
assert config["pytest_cmd"] == "jest"
|
|
assert path == package_json
|
|
|
|
def test_parses_typescript_project(self, tmp_path: Path) -> None:
|
|
"""Should detect TypeScript project."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test"}))
|
|
(tmp_path / "tsconfig.json").write_text("{}")
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["language"] == "typescript"
|
|
|
|
def test_auto_detects_module_root(self, tmp_path: Path) -> None:
|
|
"""Should auto-detect module root from package.json."""
|
|
(tmp_path / "src").mkdir()
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "main": "./src/index.js"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["module_root"] == str((tmp_path / "src").resolve())
|
|
|
|
def test_respects_module_root_override(self, tmp_path: Path) -> None:
|
|
"""Should respect moduleRoot override in codeflash config."""
|
|
(tmp_path / "lib").mkdir()
|
|
(tmp_path / "src").mkdir()
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps({"name": "test", "main": "./src/index.js", "codeflash": {"moduleRoot": "lib"}})
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["module_root"] == str((tmp_path / "lib").resolve())
|
|
|
|
def test_auto_detects_formatter(self, tmp_path: Path) -> None:
|
|
"""Should auto-detect formatter from devDependencies."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "devDependencies": {"prettier": "^3.0.0"}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["formatter_cmds"] == ["npx prettier --write $file"]
|
|
|
|
def test_respects_formatter_override(self, tmp_path: Path) -> None:
|
|
"""Should respect formatterCmds override."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "test",
|
|
"devDependencies": {"prettier": "^3.0.0"},
|
|
"codeflash": {"formatterCmds": ["custom-formatter $file"]},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["formatter_cmds"] == ["custom-formatter $file"]
|
|
|
|
def test_parses_ignore_paths(self, tmp_path: Path) -> None:
|
|
"""Should parse ignorePaths from codeflash config."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": {"ignorePaths": ["dist", "node_modules"]}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert str((tmp_path / "dist").resolve()) in config["ignore_paths"]
|
|
assert str((tmp_path / "node_modules").resolve()) in config["ignore_paths"]
|
|
|
|
def test_parses_benchmarks_root(self, tmp_path: Path) -> None:
|
|
"""Should parse benchmarksRoot from codeflash config."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": {"benchmarksRoot": "__benchmarks__"}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["benchmarks_root"] == str((tmp_path / "__benchmarks__").resolve())
|
|
|
|
def test_parses_disable_telemetry(self, tmp_path: Path) -> None:
|
|
"""Should parse disableTelemetry from codeflash config."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": {"disableTelemetry": True}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["disable_telemetry"] is True
|
|
|
|
def test_defaults_disable_telemetry_to_false(self, tmp_path: Path) -> None:
|
|
"""Should default disableTelemetry to False."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["disable_telemetry"] is False
|
|
|
|
def test_sets_backwards_compat_defaults(self, tmp_path: Path) -> None:
|
|
"""Should set backwards compatibility defaults."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["git_remote"] == "origin"
|
|
assert config["disable_imports_sorting"] is False
|
|
assert config["override_fixtures"] is False
|
|
|
|
def test_returns_none_for_invalid_json(self, tmp_path: Path) -> None:
|
|
"""Should return None for invalid JSON."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text("invalid json")
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is None
|
|
|
|
def test_handles_non_dict_codeflash_config(self, tmp_path: Path) -> None:
|
|
"""Should handle non-dict codeflash section."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": "invalid"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
# Should use auto-detected/default values
|
|
assert "language" in config
|
|
|
|
def test_empty_formatter_when_none_detected(self, tmp_path: Path) -> None:
|
|
"""Should have empty formatter_cmds when no formatter detected."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "devDependencies": {"typescript": "^5.0.0"}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["formatter_cmds"] == []
|
|
|
|
def test_parses_git_remote_from_config(self, tmp_path: Path) -> None:
|
|
"""Should parse gitRemote from codeflash config."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": {"gitRemote": "upstream"}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["git_remote"] == "upstream"
|
|
|
|
def test_defaults_git_remote_to_origin(self, tmp_path: Path) -> None:
|
|
"""Should default gitRemote to 'origin' when not specified."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["git_remote"] == "origin"
|
|
|
|
def test_handles_empty_git_remote(self, tmp_path: Path) -> None:
|
|
"""Should handle empty gitRemote in config."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test", "codeflash": {"gitRemote": ""}}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
# Empty string should be treated as the value (not defaulted to origin)
|
|
assert config["git_remote"] == ""
|
|
|
|
|
|
class TestClearCache:
|
|
"""Tests for clear_cache function."""
|
|
|
|
def test_clears_both_caches(self, tmp_path: Path) -> None:
|
|
"""Should clear both path and data caches."""
|
|
# Populate caches
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "test"}))
|
|
get_package_json_data(package_json)
|
|
|
|
assert len(PACKAGE_JSON_DATA_CACHE) > 0
|
|
|
|
clear_cache()
|
|
|
|
assert len(PACKAGE_JSON_CACHE) == 0
|
|
assert len(PACKAGE_JSON_DATA_CACHE) == 0
|
|
|
|
|
|
class TestRealWorldPackageJsonExamples:
|
|
"""Tests with real-world-like package.json configurations."""
|
|
|
|
def test_nextjs_project(self, tmp_path: Path) -> None:
|
|
"""Should handle Next.js project configuration."""
|
|
(tmp_path / "src").mkdir()
|
|
(tmp_path / "tsconfig.json").write_text("{}")
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "my-nextjs-app",
|
|
"scripts": {"test": "jest"},
|
|
"devDependencies": {"jest": "^29.0.0", "prettier": "^3.0.0", "typescript": "^5.0.0"},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["language"] == "typescript"
|
|
assert config["module_root"] == str((tmp_path / "src").resolve())
|
|
assert config["test_framework"] == "jest"
|
|
assert config["formatter_cmds"] == ["npx prettier --write $file"]
|
|
|
|
def test_vite_react_project(self, tmp_path: Path) -> None:
|
|
"""Should handle Vite + React project configuration."""
|
|
(tmp_path / "src").mkdir()
|
|
(tmp_path / "tsconfig.json").write_text("{}")
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "vite-react-app",
|
|
"type": "module",
|
|
"scripts": {"test": "vitest"},
|
|
"devDependencies": {"vitest": "^1.0.0", "eslint": "^8.0.0"},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["language"] == "typescript"
|
|
assert config["test_framework"] == "vitest"
|
|
assert config["formatter_cmds"] == ["npx eslint --fix $file"]
|
|
|
|
def test_library_with_exports(self, tmp_path: Path) -> None:
|
|
"""Should handle library with modern exports field, skipping build output dirs."""
|
|
(tmp_path / "dist").mkdir()
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "my-library",
|
|
"exports": {".": {"import": "./dist/index.mjs", "require": "./dist/index.cjs"}},
|
|
"devDependencies": {"vitest": "^1.0.0", "prettier": "^3.0.0"},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
# dist is a build output directory, so it's skipped and falls back to project root
|
|
assert config["module_root"] == str(tmp_path.resolve())
|
|
|
|
def test_monorepo_package(self, tmp_path: Path) -> None:
|
|
"""Should handle monorepo package configuration."""
|
|
(tmp_path / "packages" / "core" / "src").mkdir(parents=True)
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{"name": "@myorg/core", "main": "./packages/core/src/index.js", "devDependencies": {"jest": "^29.0.0"}}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["module_root"] == str((tmp_path / "packages/core/src").resolve())
|
|
|
|
def test_node_cli_project(self, tmp_path: Path) -> None:
|
|
"""Should handle Node.js CLI project."""
|
|
(tmp_path / "bin").mkdir()
|
|
(tmp_path / "lib").mkdir()
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "my-cli",
|
|
"bin": {"my-cli": "./bin/cli.js"},
|
|
"main": "./lib/index.js",
|
|
"devDependencies": {"mocha": "^10.0.0"},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["module_root"] == str((tmp_path / "lib").resolve())
|
|
assert config["test_framework"] == "mocha"
|
|
|
|
def test_minimal_project(self, tmp_path: Path) -> None:
|
|
"""Should handle minimal package.json."""
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(json.dumps({"name": "minimal"}))
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["language"] == "javascript"
|
|
assert config["module_root"] == str(tmp_path.resolve())
|
|
assert config["test_framework"] == "jest"
|
|
assert config["formatter_cmds"] == []
|
|
|
|
def test_existing_codeflash_config_with_overrides(self, tmp_path: Path) -> None:
|
|
"""Should handle existing codeflash config with custom overrides."""
|
|
(tmp_path / "custom-src").mkdir()
|
|
package_json = tmp_path / "package.json"
|
|
package_json.write_text(
|
|
json.dumps(
|
|
{
|
|
"name": "configured-project",
|
|
"devDependencies": {"jest": "^29.0.0", "prettier": "^3.0.0"},
|
|
"codeflash": {
|
|
"moduleRoot": "custom-src",
|
|
"formatterCmds": ["npx prettier --write --single-quote $file"],
|
|
"ignorePaths": ["dist", "coverage"],
|
|
"disableTelemetry": True,
|
|
},
|
|
}
|
|
)
|
|
)
|
|
|
|
result = parse_package_json_config(package_json)
|
|
|
|
assert result is not None
|
|
config, _ = result
|
|
assert config["module_root"] == str((tmp_path / "custom-src").resolve())
|
|
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"]
|