"""Tests for the universal project detector.""" import json from codeflash.setup.detector import ( _detect_js_formatter, _detect_js_module_root, _detect_js_test_runner, _detect_language, _detect_python_formatter, _detect_python_module_root, _detect_python_test_runner, _detect_tests_root, _find_project_root, detect_project, has_existing_config, is_build_output_dir, ) class TestFindProjectRoot: """Tests for _find_project_root function.""" def test_finds_git_directory(self, tmp_path): """Should find project root by .git directory.""" (tmp_path / ".git").mkdir() subdir = tmp_path / "src" / "deep" subdir.mkdir(parents=True) result = _find_project_root(subdir) assert result == tmp_path def test_finds_pyproject_toml(self, tmp_path): """Should find project root by pyproject.toml.""" (tmp_path / "pyproject.toml").write_text("[project]\nname = 'test'") subdir = tmp_path / "src" subdir.mkdir() result = _find_project_root(subdir) assert result == tmp_path def test_finds_package_json(self, tmp_path): """Should find project root by package.json.""" (tmp_path / "package.json").write_text('{"name": "test"}') subdir = tmp_path / "lib" subdir.mkdir() result = _find_project_root(subdir) assert result == tmp_path def test_returns_none_when_no_markers(self, tmp_path): """Should return None when no project markers found.""" subdir = tmp_path / "orphan" subdir.mkdir() result = _find_project_root(subdir) # Will walk up to filesystem root and not find anything assert result is None or result == tmp_path class TestDetectLanguage: """Tests for _detect_language function.""" def test_detects_typescript_from_tsconfig(self, tmp_path): """Should detect TypeScript when tsconfig.json exists.""" (tmp_path / "tsconfig.json").write_text("{}") (tmp_path / "package.json").write_text('{"name": "test"}') lang, confidence, detail = _detect_language(tmp_path) assert lang == "typescript" assert confidence == 1.0 assert "tsconfig.json" in detail def test_detects_python_from_pyproject(self, tmp_path): """Should detect Python when pyproject.toml exists.""" (tmp_path / "pyproject.toml").write_text("[project]\nname = 'test'") lang, confidence, detail = _detect_language(tmp_path) assert lang == "python" assert confidence == 1.0 assert "pyproject.toml" in detail def test_detects_python_from_setup_py(self, tmp_path): """Should detect Python when setup.py exists.""" (tmp_path / "setup.py").write_text("from setuptools import setup\nsetup()") lang, confidence, detail = _detect_language(tmp_path) assert lang == "python" assert confidence == 1.0 assert "setup.py" in detail def test_detects_javascript_from_package_json(self, tmp_path): """Should detect JavaScript when only package.json exists.""" (tmp_path / "package.json").write_text('{"name": "test"}') lang, confidence, detail = _detect_language(tmp_path) assert lang == "javascript" assert confidence == 0.9 assert "package.json" in detail def test_defaults_to_python(self, tmp_path): """Should default to Python when no markers found.""" lang, confidence, detail = _detect_language(tmp_path) assert lang == "python" assert confidence < 0.5 # Low confidence class TestDetectModuleRoot: """Tests for module root detection.""" def test_python_detects_src_layout(self, tmp_path): """Should detect src/ layout for Python.""" src_dir = tmp_path / "src" / "mypackage" src_dir.mkdir(parents=True) (src_dir / "__init__.py").write_text("") module_root, detail = _detect_python_module_root(tmp_path) assert module_root == src_dir assert module_root.name == "mypackage" assert module_root.parent.name == "src" def test_python_detects_package_at_root(self, tmp_path): """Should detect package at project root.""" pkg_dir = tmp_path / "mypackage" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") module_root, detail = _detect_python_module_root(tmp_path) assert module_root == pkg_dir def test_python_uses_pyproject_name(self, tmp_path): """Should use project name from pyproject.toml.""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "myapp"') pkg_dir = tmp_path / "myapp" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") module_root, detail = _detect_python_module_root(tmp_path) assert module_root == pkg_dir assert "pyproject.toml" in detail def test_js_detects_from_exports(self, tmp_path): """Should detect module root from package.json exports when no common src dir exists.""" (tmp_path / "package.json").write_text( json.dumps({"name": "test", "exports": {".": "./packages/core/index.js"}}) ) (tmp_path / "packages" / "core").mkdir(parents=True) module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "packages" / "core" assert "exports" in detail def test_js_detects_src_convention(self, tmp_path): """Should detect src/ directory for JS.""" (tmp_path / "package.json").write_text('{"name": "test"}') (tmp_path / "src").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "src" def test_js_prefers_src_over_build_src(self, tmp_path): """Should prefer src/ over build/src/ even when package.json points to build/.""" (tmp_path / "package.json").write_text( json.dumps({"name": "test", "main": "build/src/index.js", "module": "build/src/index.js"}) ) (tmp_path / "src").mkdir() (tmp_path / "build" / "src").mkdir(parents=True) module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "src" assert "src/ directory" in detail def test_js_skips_build_dir_from_main(self, tmp_path): """Should skip build output directories from package.json main field.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "main": "build/index.js"})) (tmp_path / "build").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path assert "project root" in detail def test_js_skips_dist_dir_from_exports(self, tmp_path): """Should skip dist output directories from package.json exports field.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "exports": {".": "./dist/index.js"}})) (tmp_path / "dist").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path assert "project root" in detail def test_js_skips_out_dir_from_module(self, tmp_path): """Should skip out output directories from package.json module field.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "module": "out/esm/index.js"})) (tmp_path / "out" / "esm").mkdir(parents=True) module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path assert "project root" in detail def test_js_prefers_lib_over_build_dir(self, tmp_path): """Should prefer lib/ over build output directories.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "main": "dist/index.js"})) (tmp_path / "lib").mkdir() (tmp_path / "dist").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "lib" assert "lib/ directory" in detail def test_js_prefers_source_over_build_dir(self, tmp_path): """Should prefer source/ over build output directories.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "main": "build/index.js"})) (tmp_path / "source").mkdir() (tmp_path / "build").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "source" assert "source/ directory" in detail def test_js_falls_back_to_valid_exports_path(self, tmp_path): """Should use exports path when no common source dirs exist and path is not build output.""" (tmp_path / "package.json").write_text( json.dumps({"name": "test", "exports": {".": "./packages/core/index.js"}}) ) (tmp_path / "packages" / "core").mkdir(parents=True) module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "packages" / "core" assert "exports" in detail def test_js_falls_back_to_valid_main_path(self, tmp_path): """Should use main path when no common source dirs exist and path is not build output.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "main": "packages/main/index.js"})) (tmp_path / "packages" / "main").mkdir(parents=True) module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "packages" / "main" assert "main" in detail def test_js_falls_back_to_valid_module_path(self, tmp_path): """Should use module path when no common source dirs exist and path is not build output.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "module": "esm/index.js"})) (tmp_path / "esm").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path / "esm" assert "module" in detail def test_js_returns_project_root_when_all_paths_are_build_output(self, tmp_path): """Should return project root when all package.json paths point to build outputs.""" (tmp_path / "package.json").write_text( json.dumps( { "name": "test", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "exports": {".": "./build/index.js"}, } ) ) (tmp_path / "dist" / "cjs").mkdir(parents=True) (tmp_path / "dist" / "esm").mkdir(parents=True) (tmp_path / "build").mkdir() module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path assert "project root" in detail def test_js_handles_malformed_package_json(self, tmp_path): """Should handle malformed package.json gracefully.""" (tmp_path / "package.json").write_text("{ invalid json }") module_root, detail = _detect_js_module_root(tmp_path) assert module_root == tmp_path assert "project root" in detail class TestIsBuildOutputDir: """Tests for is_build_output_dir function.""" def test_detects_build_dir(self): """Should detect build/ as build output.""" from pathlib import Path assert is_build_output_dir(Path("build")) assert is_build_output_dir(Path("build/src")) assert is_build_output_dir(Path("build/src/index.js")) def test_detects_dist_dir(self): """Should detect dist/ as build output.""" from pathlib import Path assert is_build_output_dir(Path("dist")) assert is_build_output_dir(Path("dist/esm")) assert is_build_output_dir(Path("dist/cjs/index.js")) def test_detects_out_dir(self): """Should detect out/ as build output.""" from pathlib import Path assert is_build_output_dir(Path("out")) assert is_build_output_dir(Path("out/src")) def test_detects_next_dir(self): """Should detect .next/ as build output.""" from pathlib import Path assert is_build_output_dir(Path(".next")) assert is_build_output_dir(Path(".next/static")) def test_detects_nuxt_dir(self): """Should detect .nuxt/ as build output.""" from pathlib import Path assert is_build_output_dir(Path(".nuxt")) assert is_build_output_dir(Path(".nuxt/dist")) def test_detects_nested_build_dir(self): """Should detect build dir nested in path.""" from pathlib import Path assert is_build_output_dir(Path("packages/build/index.js")) assert is_build_output_dir(Path("foo/dist/bar")) def test_does_not_detect_src(self): """Should not detect src/ as build output.""" from pathlib import Path assert not is_build_output_dir(Path("src")) assert not is_build_output_dir(Path("src/index.js")) def test_does_not_detect_lib(self): """Should not detect lib/ as build output.""" from pathlib import Path assert not is_build_output_dir(Path("lib")) assert not is_build_output_dir(Path("lib/utils")) def test_does_not_detect_source(self): """Should not detect source/ as build output.""" from pathlib import Path assert not is_build_output_dir(Path("source")) def test_does_not_detect_packages(self): """Should not detect packages/ as build output.""" from pathlib import Path assert not is_build_output_dir(Path("packages")) assert not is_build_output_dir(Path("packages/core")) def test_does_not_detect_similar_names(self): """Should not detect directories with similar but different names.""" from pathlib import Path assert not is_build_output_dir(Path("builder")) assert not is_build_output_dir(Path("distribution")) assert not is_build_output_dir(Path("output")) class TestDetectTestsRoot: """Tests for tests root detection.""" def test_detects_tests_directory(self, tmp_path): """Should detect tests/ directory.""" (tmp_path / "tests").mkdir() tests_root, detail = _detect_tests_root(tmp_path, "python") assert tests_root == tmp_path / "tests" def test_detects_test_directory(self, tmp_path): """Should detect test/ directory.""" (tmp_path / "test").mkdir() tests_root, detail = _detect_tests_root(tmp_path, "python") assert tests_root == tmp_path / "test" def test_detects_dunder_tests(self, tmp_path): """Should detect __tests__/ directory (JS convention).""" (tmp_path / "__tests__").mkdir() tests_root, detail = _detect_tests_root(tmp_path, "javascript") assert tests_root == tmp_path / "__tests__" def test_returns_none_when_not_found(self, tmp_path): """Should return None when no tests directory found.""" tests_root, detail = _detect_tests_root(tmp_path, "python") assert tests_root is None class TestDetectTestRunner: """Tests for test runner detection.""" def test_python_detects_pytest_from_ini(self, tmp_path): """Should detect pytest from pytest.ini.""" (tmp_path / "pytest.ini").write_text("[pytest]") runner, detail = _detect_python_test_runner(tmp_path) assert runner == "pytest" def test_python_detects_pytest_from_conftest(self, tmp_path): """Should detect pytest from conftest.py.""" (tmp_path / "conftest.py").write_text("import pytest") runner, detail = _detect_python_test_runner(tmp_path) assert runner == "pytest" def test_js_detects_jest_from_deps(self, tmp_path): """Should detect jest from devDependencies.""" (tmp_path / "package.json").write_text(json.dumps({"devDependencies": {"jest": "^29.0.0"}})) runner, detail = _detect_js_test_runner(tmp_path) assert runner == "jest" def test_js_detects_vitest_from_deps(self, tmp_path): """Should detect vitest from devDependencies (preferred over jest).""" (tmp_path / "package.json").write_text(json.dumps({"devDependencies": {"vitest": "^1.0.0", "jest": "^29.0.0"}})) runner, detail = _detect_js_test_runner(tmp_path) assert runner == "vitest" def test_js_detects_from_config_file(self, tmp_path): """Should detect test runner from config file.""" (tmp_path / "package.json").write_text('{"name": "test"}') (tmp_path / "vitest.config.js").write_text("export default {}") runner, detail = _detect_js_test_runner(tmp_path) assert runner == "vitest" class TestDetectFormatter: """Tests for formatter detection.""" def test_python_detects_ruff(self, tmp_path): """Should detect ruff from pyproject.toml.""" (tmp_path / "pyproject.toml").write_text("[tool.ruff]\nline-length = 120") formatter, detail = _detect_python_formatter(tmp_path) assert any("ruff" in cmd for cmd in formatter) def test_python_detects_black(self, tmp_path): """Should detect black from pyproject.toml.""" (tmp_path / "pyproject.toml").write_text("[tool.black]\nline-length = 88") formatter, detail = _detect_python_formatter(tmp_path) assert any("black" in cmd for cmd in formatter) def test_js_detects_prettier(self, tmp_path): """Should detect prettier from config file.""" (tmp_path / "package.json").write_text('{"name": "test"}') (tmp_path / ".prettierrc").write_text("{}") formatter, detail = _detect_js_formatter(tmp_path) assert any("prettier" in cmd for cmd in formatter) def test_js_detects_prettier_from_deps(self, tmp_path): """Should detect prettier from devDependencies.""" (tmp_path / "package.json").write_text(json.dumps({"devDependencies": {"prettier": "^3.0.0"}})) formatter, detail = _detect_js_formatter(tmp_path) assert any("prettier" in cmd for cmd in formatter) class TestDetectProject: """Integration tests for detect_project function.""" def test_detects_python_project(self, tmp_path): """Should correctly detect a Python project.""" # Create Python project structure (tmp_path / "pyproject.toml").write_text('[project]\nname = "myapp"\n\n[tool.ruff]\nline-length = 120') (tmp_path / "myapp").mkdir() (tmp_path / "myapp" / "__init__.py").write_text("") (tmp_path / "tests").mkdir() (tmp_path / ".git").mkdir() detected = detect_project(tmp_path) assert detected.language == "python" assert detected.project_root == tmp_path assert detected.module_root == tmp_path / "myapp" assert detected.tests_root == tmp_path / "tests" assert detected.test_runner == "pytest" assert any("ruff" in cmd for cmd in detected.formatter_cmds) def test_detects_javascript_project(self, tmp_path): """Should correctly detect a JavaScript project.""" # Create JS project structure (tmp_path / "package.json").write_text( json.dumps({"name": "myapp", "devDependencies": {"jest": "^29.0.0", "prettier": "^3.0.0"}}) ) (tmp_path / "src").mkdir() (tmp_path / "tests").mkdir() (tmp_path / ".git").mkdir() detected = detect_project(tmp_path) assert detected.language == "javascript" assert detected.project_root == tmp_path assert detected.module_root == tmp_path / "src" assert detected.tests_root == tmp_path / "tests" assert detected.test_runner == "jest" assert any("prettier" in cmd for cmd in detected.formatter_cmds) def test_detects_typescript_project(self, tmp_path): """Should correctly detect a TypeScript project.""" # Create TS project structure (tmp_path / "package.json").write_text( json.dumps({"name": "myapp", "devDependencies": {"vitest": "^1.0.0", "typescript": "^5.0.0"}}) ) (tmp_path / "tsconfig.json").write_text("{}") (tmp_path / "src").mkdir() (tmp_path / ".git").mkdir() detected = detect_project(tmp_path) assert detected.language == "typescript" assert detected.test_runner == "vitest" def test_to_display_dict(self, tmp_path): """Should generate display dictionary correctly.""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "test"') (tmp_path / "test").mkdir() (tmp_path / "test" / "__init__.py").write_text("") (tmp_path / "tests").mkdir() detected = detect_project(tmp_path) display = detected.to_display_dict() assert "Language" in display assert "Module root" in display assert "Test runner" in display class TestHasExistingConfig: """Tests for has_existing_config function.""" def test_detects_pyproject_config(self, tmp_path): """Should detect config in pyproject.toml.""" (tmp_path / "pyproject.toml").write_text('[tool.codeflash]\nmodule-root = "src"') has_config, config_type = has_existing_config(tmp_path) assert has_config is True assert config_type == "pyproject.toml" def test_detects_package_json_config(self, tmp_path): """Should detect config in package.json.""" (tmp_path / "package.json").write_text(json.dumps({"name": "test", "codeflash": {"moduleRoot": "src"}})) has_config, config_type = has_existing_config(tmp_path) assert has_config is True assert config_type == "package.json" def test_returns_false_when_no_config(self, tmp_path): """Should return False when no codeflash config exists.""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "test"') has_config, config_type = has_existing_config(tmp_path) assert has_config is False assert config_type is None def test_returns_false_for_empty_directory(self, tmp_path): """Should return False for empty directory.""" has_config, config_type = has_existing_config(tmp_path) assert has_config is False assert config_type is None