From 516ec792287195321b2688470ed45d41fe5ce24b Mon Sep 17 00:00:00 2001 From: ydliu2 Date: Sat, 2 May 2026 19:50:23 +0800 Subject: [PATCH] test: achieve 100% module coverage with analyze_project and plugin_manager tests Add test suites for the final 2 core modules: - test_analyze_project.py (37 tests) - project type detection, monorepo tools, package managers, module discovery, documentation finding - test_plugin_manager.py (19 tests) - registry CRUD, manifest parsing, plugin listing, enable/disable, uninstall Total: 150 test cases across 8/8 modules, 2,193 lines of test code Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_analyze_project.py | 274 ++++++++++++++++++++++++++++++++++ tests/test_plugin_manager.py | 243 ++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 tests/test_analyze_project.py create mode 100644 tests/test_plugin_manager.py diff --git a/tests/test_analyze_project.py b/tests/test_analyze_project.py new file mode 100644 index 0000000..b7a084e --- /dev/null +++ b/tests/test_analyze_project.py @@ -0,0 +1,274 @@ +"""Tests for scripts/analyze_project.py.""" + +import json +from pathlib import Path + +from analyze_project import ( + categorize_module, + detect_monorepo_tools, + detect_package_manager, + detect_project_types, + discover_modules, + find_documentation, + find_entry_points, + analyze_project, + IGNORE_DIRS, + CODE_EXTENSIONS, +) + + +# --- categorize_module --- + + +def test_categorize_ui_modules(): + for name in ["components", "ui-kit", "views", "pages"]: + assert categorize_module(name) == "ui" + + +def test_categorize_api_modules(): + for name in ["api", "services", "handler"]: + assert categorize_module(name) == "api" + + +def test_categorize_utility_modules(): + for name in ["utils", "helpers", "common", "shared"]: + assert categorize_module(name) == "utility" + + +def test_categorize_core_modules(): + for name in ["core", "lib", "engine"]: + assert categorize_module(name) == "core" + + +def test_categorize_config_modules(): + assert categorize_module("config") == "config" + assert categorize_module("settings") == "config" + + +def test_categorize_test_modules(): + assert categorize_module("tests") == "test" + assert categorize_module("spec") == "test" + + +def test_categorize_unknown_module(): + assert categorize_module("random-name") == "module" + + +# --- detect_package_manager --- + + +def test_detect_npm(tmp_path): + (tmp_path / "package-lock.json").write_text("{}") + assert "npm" in detect_package_manager(tmp_path) + + +def test_detect_yarn(tmp_path): + (tmp_path / "yarn.lock").write_text("") + assert "yarn" in detect_package_manager(tmp_path) + + +def test_detect_pnpm(tmp_path): + (tmp_path / "pnpm-lock.yaml").write_text("") + assert "pnpm" in detect_package_manager(tmp_path) + + +def test_detect_bun(tmp_path): + (tmp_path / "bun.lockb").write_bytes(b"") + assert "bun" in detect_package_manager(tmp_path) + + +def test_detect_no_package_manager(tmp_path): + assert detect_package_manager(tmp_path) == [] + + +# --- detect_monorepo_tools --- + + +def test_detect_turborepo(tmp_path): + (tmp_path / "turbo.json").write_text("{}") + result = detect_monorepo_tools(tmp_path) + assert "turborepo" in result + assert "monorepo" in result + + +def test_detect_lerna(tmp_path): + (tmp_path / "lerna.json").write_text("{}") + result = detect_monorepo_tools(tmp_path) + assert "lerna" in result + assert "monorepo" in result + + +def test_detect_pnpm_workspaces(tmp_path): + (tmp_path / "pnpm-workspace.yaml").write_text("packages:\n - packages/*") + result = detect_monorepo_tools(tmp_path) + assert "pnpm-workspaces" in result + + +def test_detect_npm_workspaces(tmp_path): + (tmp_path / "package.json").write_text(json.dumps({"workspaces": ["packages/*"]})) + result = detect_monorepo_tools(tmp_path) + assert "npm-workspaces" in result + + +def test_detect_no_monorepo(tmp_path): + assert detect_monorepo_tools(tmp_path) == [] + + +# --- detect_project_types --- + + +def test_detect_python_project(tmp_path): + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'test'\n") + types = detect_project_types(tmp_path) + assert "python" in types + + +def test_detect_nodejs_project(tmp_path): + (tmp_path / "package.json").write_text(json.dumps({"name": "test"})) + types = detect_project_types(tmp_path) + assert "nodejs" in types + + +def test_detect_go_project(tmp_path): + (tmp_path / "go.mod").write_text("module example.com/test\n\ngo 1.21\n") + types = detect_project_types(tmp_path) + assert "go" in types + + +def test_detect_rust_project(tmp_path): + (tmp_path / "Cargo.toml").write_text('[package]\nname = "test"\n') + types = detect_project_types(tmp_path) + assert "rust" in types + + +def test_detect_react_in_package_json(tmp_path): + pkg = {"name": "test", "dependencies": {"react": "^18.0.0"}} + (tmp_path / "package.json").write_text(json.dumps(pkg)) + types = detect_project_types(tmp_path) + assert "react" in types + + +def test_detect_empty_project(tmp_path): + assert detect_project_types(tmp_path) == [] + + +# --- find_entry_points --- + + +def test_find_entry_points_python(tmp_path): + (tmp_path / "main.py").write_text("print('hello')") + entries = find_entry_points(tmp_path, ["python"]) + assert "main.py" in entries + + +def test_find_entry_points_node(tmp_path): + (tmp_path / "src").mkdir() + (tmp_path / "src" / "index.ts").write_text("export default {}") + entries = find_entry_points(tmp_path, ["nodejs", "typescript"]) + assert "src/index.ts" in entries + + +def test_find_entry_points_empty(tmp_path): + assert find_entry_points(tmp_path, []) == [] + + +# --- discover_modules --- + + +def test_discover_modules_src_dir(tmp_path): + src = tmp_path / "src" + (src / "core").mkdir(parents=True) + (src / "core" / "app.py").write_text("pass") + (src / "utils").mkdir() + (src / "utils" / "helpers.py").write_text("pass") + + modules = discover_modules(tmp_path) + names = [m["name"] for m in modules] + assert "core" in names + assert "utils" in names + + +def test_discover_modules_fallback_to_root(tmp_path): + (tmp_path / "scripts").mkdir() + (tmp_path / "scripts" / "run.py").write_text("pass") + + modules = discover_modules(tmp_path) + names = [m["name"] for m in modules] + assert "scripts" in names + + +def test_discover_modules_empty(tmp_path): + assert discover_modules(tmp_path) == [] + + +def test_discover_modules_ignores_excluded_dirs(tmp_path): + src = tmp_path / "src" + (src / "node_modules").mkdir(parents=True) + (src / "node_modules" / "pkg.js").write_text("pass") + + modules = discover_modules(tmp_path) + names = [m["name"] for m in modules] + assert "node_modules" not in names + + +# --- find_documentation --- + + +def test_find_documentation_readme(tmp_path): + (tmp_path / "README.md").write_text("# Hello") + docs = find_documentation(tmp_path) + assert "README.md" in docs + + +def test_find_documentation_multiple(tmp_path): + (tmp_path / "README.md").write_text("# Hello") + (tmp_path / "CHANGELOG.md").write_text("# Changes") + (tmp_path / "LICENSE").write_text("MIT") + docs = find_documentation(tmp_path) + assert len(docs) >= 3 + + +def test_find_documentation_docs_dir(tmp_path): + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "guide.md").write_text("# Guide") + docs = find_documentation(tmp_path) + assert any("guide.md" in d for d in docs) + + +def test_find_documentation_empty(tmp_path): + assert find_documentation(tmp_path) == [] + + +# --- analyze_project --- + + +def test_analyze_project_basic(tmp_path): + (tmp_path / "main.py").write_text("print('hello')") + (tmp_path / "README.md").write_text("# Test") + + result = analyze_project(str(tmp_path), save_to_cache=False) + + assert result["project_name"] == tmp_path.name + assert "stats" in result + assert result["stats"]["total_docs"] >= 1 + assert "analyzed_at" in result + + +def test_analyze_project_saves_cache(tmp_path): + wiki_dir = tmp_path / ".mini-wiki" / "cache" + wiki_dir.mkdir(parents=True) + + (tmp_path / "app.py").write_text("pass") + + analyze_project(str(tmp_path), save_to_cache=True) + + cache_file = tmp_path / ".mini-wiki" / "cache" / "structure.json" + assert cache_file.exists() + data = json.loads(cache_file.read_text()) + assert "project_name" in data + + +def test_analyze_project_empty(tmp_path): + result = analyze_project(str(tmp_path), save_to_cache=False) + assert result["stats"]["total_files"] == 0 + assert result["modules"] == [] diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py new file mode 100644 index 0000000..48819d2 --- /dev/null +++ b/tests/test_plugin_manager.py @@ -0,0 +1,243 @@ +"""Tests for scripts/plugin_manager.py.""" + +import json +from pathlib import Path + +import yaml + +from plugin_manager import ( + get_plugins_dir, + get_registry_path, + load_registry, + save_registry, + parse_plugin_manifest, + list_plugins, + enable_plugin, + uninstall_plugin, +) + + +# --- path helpers --- + + +def test_get_plugins_dir(tmp_path): + result = get_plugins_dir(str(tmp_path)) + assert result == tmp_path / "plugins" + + +def test_get_registry_path(tmp_path): + result = get_registry_path(str(tmp_path)) + assert result == tmp_path / "plugins" / "_registry.yaml" + + +# --- load_registry / save_registry --- + + +def test_load_registry_empty(tmp_path): + result = load_registry(str(tmp_path)) + assert result == {"plugins": []} + + +def test_load_registry_existing(tmp_path): + reg_path = tmp_path / "plugins" / "_registry.yaml" + reg_path.parent.mkdir(parents=True) + reg_path.write_text(yaml.dump({"plugins": [{"name": "test", "enabled": True}]})) + + result = load_registry(str(tmp_path)) + assert len(result["plugins"]) == 1 + assert result["plugins"][0]["name"] == "test" + + +def test_save_registry_creates_file(tmp_path): + registry = {"plugins": [{"name": "foo", "enabled": True}]} + save_registry(str(tmp_path), registry) + + reg_path = tmp_path / "plugins" / "_registry.yaml" + assert reg_path.exists() + loaded = yaml.safe_load(reg_path.read_text()) + assert loaded["plugins"][0]["name"] == "foo" + + +def test_save_and_load_roundtrip(tmp_path): + original = {"plugins": [ + {"name": "a", "enabled": True, "priority": 10}, + {"name": "b", "enabled": False, "priority": 20}, + ]} + save_registry(str(tmp_path), original) + loaded = load_registry(str(tmp_path)) + assert len(loaded["plugins"]) == 2 + assert loaded["plugins"][1]["name"] == "b" + + +# --- parse_plugin_manifest --- + + +def _create_plugin(tmp_path, name, manifest_content): + """Helper to create a plugin directory with PLUGIN.md.""" + plugin_dir = tmp_path / "plugins" / name + plugin_dir.mkdir(parents=True) + (plugin_dir / "PLUGIN.md").write_text(manifest_content) + return plugin_dir + + +def test_parse_plugin_manifest_valid(tmp_path): + plugin_dir = _create_plugin(tmp_path, "test-plugin", """--- +name: test-plugin +type: analyzer +version: 1.0.0 +description: A test plugin +--- + +# Test Plugin +""") + result = parse_plugin_manifest(plugin_dir) + assert result is not None + assert result["name"] == "test-plugin" + assert result["type"] == "analyzer" + assert result["version"] == "1.0.0" + + +def test_parse_plugin_manifest_no_frontmatter(tmp_path): + plugin_dir = _create_plugin(tmp_path, "no-fm", "# Just a heading\nNo frontmatter here.") + result = parse_plugin_manifest(plugin_dir) + assert result is None + + +def test_parse_plugin_manifest_no_file(tmp_path): + plugin_dir = tmp_path / "plugins" / "empty" + plugin_dir.mkdir(parents=True) + result = parse_plugin_manifest(plugin_dir) + assert result is None + + +def test_parse_plugin_manifest_invalid_yaml(tmp_path): + plugin_dir = _create_plugin(tmp_path, "bad-yaml", """--- +name: [invalid yaml + broken: { +--- +""") + result = parse_plugin_manifest(plugin_dir) + assert result is None + + +# --- list_plugins --- + + +def test_list_plugins_empty(tmp_path): + result = list_plugins(str(tmp_path)) + assert result == [] + + +def test_list_plugins_with_plugins(tmp_path): + _create_plugin(tmp_path, "plugin-a", """--- +name: plugin-a +type: analyzer +version: 1.0.0 +description: Plugin A +--- +# Plugin A +""") + _create_plugin(tmp_path, "plugin-b", """--- +name: plugin-b +type: generator +version: 2.0.0 +description: Plugin B +--- +# Plugin B +""") + + result = list_plugins(str(tmp_path)) + names = [p["name"] for p in result] + assert "plugin-a" in names + assert "plugin-b" in names + + +def test_list_plugins_skips_underscore_dirs(tmp_path): + _create_plugin(tmp_path, "_example", """--- +name: _example +type: analyzer +version: 1.0.0 +description: Example +--- +""") + result = list_plugins(str(tmp_path)) + names = [p["name"] for p in result] + assert "_example" not in names + + +def test_list_plugins_respects_registry(tmp_path): + _create_plugin(tmp_path, "my-plugin", """--- +name: my-plugin +type: analyzer +version: 1.0.0 +description: My Plugin +--- +""") + registry = {"plugins": [{"name": "my-plugin", "enabled": False, "priority": 5}]} + save_registry(str(tmp_path), registry) + + result = list_plugins(str(tmp_path)) + assert len(result) == 1 + assert result[0]["enabled"] is False + assert result[0]["priority"] == 5 + + +# --- enable_plugin --- + + +def test_enable_plugin_success(tmp_path): + registry = {"plugins": [{"name": "test", "enabled": False}]} + save_registry(str(tmp_path), registry) + + result = enable_plugin(str(tmp_path), "test", True) + assert result["success"] is True + + loaded = load_registry(str(tmp_path)) + assert loaded["plugins"][0]["enabled"] is True + + +def test_disable_plugin_success(tmp_path): + registry = {"plugins": [{"name": "test", "enabled": True}]} + save_registry(str(tmp_path), registry) + + result = enable_plugin(str(tmp_path), "test", False) + assert result["success"] is True + assert "disabled" in result["message"] + + +def test_enable_plugin_not_found(tmp_path): + registry = {"plugins": []} + save_registry(str(tmp_path), registry) + + result = enable_plugin(str(tmp_path), "nonexistent", True) + assert result["success"] is False + assert "not found" in result["message"] + + +# --- uninstall_plugin --- + + +def test_uninstall_plugin_success(tmp_path): + _create_plugin(tmp_path, "to-remove", """--- +name: to-remove +type: analyzer +version: 1.0.0 +description: Will be removed +--- +""") + registry = {"plugins": [{"name": "to-remove", "enabled": True}]} + save_registry(str(tmp_path), registry) + + result = uninstall_plugin(str(tmp_path), "to-remove") + assert result["success"] is True + assert not (tmp_path / "plugins" / "to-remove").exists() + + loaded = load_registry(str(tmp_path)) + names = [p["name"] for p in loaded["plugins"]] + assert "to-remove" not in names + + +def test_uninstall_plugin_not_found(tmp_path): + result = uninstall_plugin(str(tmp_path), "nonexistent") + assert result["success"] is False + assert "not found" in result["message"]