codeflash/tests/code_utils/test_config_js.py
Kevin Turcios eceac13fc3 Merge remote-tracking branch 'origin/main' into omni-java
# 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
2026-03-04 01:52:32 -05:00

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"]