""" Tests for wizard-bootstrap tooling (Epic-004). These tests exercise the bootstrap, skills audit, and dependency checker without requiring network access or API keys. """ import json import os import sys from pathlib import Path from unittest import mock import pytest # Ensure repo root importable REPO_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(REPO_ROOT)) sys.path.insert(0, str(REPO_ROOT / "wizard-bootstrap")) import wizard_bootstrap as wb import skills_audit as sa import dependency_checker as dc # --------------------------------------------------------------------------- # wizard_bootstrap tests # --------------------------------------------------------------------------- class TestCheckPythonVersion: def test_current_python_passes(self): result = wb.check_python_version() assert result.passed assert "Python" in result.message def test_old_python_fails(self): # Patch version_info as a tuple (matches [:3] unpacking used in the check) old_info = sys.version_info try: sys.version_info = (3, 10, 0, "final", 0) # type: ignore[assignment] result = wb.check_python_version() finally: sys.version_info = old_info # type: ignore[assignment] assert not result.passed class TestCheckCoreDeps: def test_passes_when_all_present(self): result = wb.check_core_deps() # In a healthy dev environment all packages should be importable assert result.passed def test_fails_when_package_missing(self): orig = __import__ def fake_import(name, *args, **kwargs): if name == "openai": raise ModuleNotFoundError(name) return orig(name, *args, **kwargs) with mock.patch("builtins.__import__", side_effect=fake_import): with mock.patch("importlib.import_module", side_effect=ModuleNotFoundError("openai")): result = wb.check_core_deps() # With mocked importlib the check should detect the missing module assert not result.passed assert "openai" in result.message class TestCheckEnvVars: def test_fails_when_no_key_set(self): env_keys = [ "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "OPENAI_API_KEY", "GLM_API_KEY", "KIMI_API_KEY", "MINIMAX_API_KEY", ] with mock.patch.dict(os.environ, {k: "" for k in env_keys}, clear=False): # Remove all provider keys env = {k: v for k, v in os.environ.items() if k not in env_keys} with mock.patch.dict(os.environ, env, clear=True): result = wb.check_env_vars() assert not result.passed def test_passes_when_key_set(self): with mock.patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-test-key"}): result = wb.check_env_vars() assert result.passed assert "ANTHROPIC_API_KEY" in result.message class TestCheckHermesHome: def test_passes_with_existing_writable_dir(self, tmp_path): with mock.patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}): result = wb.check_hermes_home() assert result.passed def test_fails_when_dir_missing(self, tmp_path): missing = tmp_path / "nonexistent" with mock.patch.dict(os.environ, {"HERMES_HOME": str(missing)}): result = wb.check_hermes_home() assert not result.passed class TestBootstrapReport: def test_passed_when_all_pass(self): report = wb.BootstrapReport() report.add(wb.CheckResult("a", True, "ok")) report.add(wb.CheckResult("b", True, "ok")) assert report.passed assert report.failed == [] def test_failed_when_any_fail(self): report = wb.BootstrapReport() report.add(wb.CheckResult("a", True, "ok")) report.add(wb.CheckResult("b", False, "bad", fix_hint="fix it")) assert not report.passed assert len(report.failed) == 1 # --------------------------------------------------------------------------- # skills_audit tests # --------------------------------------------------------------------------- class TestSkillsAudit: def _make_skill(self, skills_root: Path, rel_path: str, content: str = "# skill") -> Path: """Create a SKILL.md at skills_root/rel_path/SKILL.md.""" skill_dir = skills_root / rel_path skill_dir.mkdir(parents=True, exist_ok=True) skill_md = skill_dir / "SKILL.md" skill_md.write_text(content) return skill_md def test_no_drift_when_identical(self, tmp_path): # run_audit expects repo_root/skills/ and installed_root/ repo = tmp_path / "repo" installed = tmp_path / "installed" content = "# Same content" self._make_skill(repo / "skills", "cat/skill-a", content) self._make_skill(installed, "cat/skill-a", content) report = sa.run_audit(repo, installed) assert not report.has_drift assert len(report.by_status("OK")) == 1 def test_detects_missing_skill(self, tmp_path): repo = tmp_path / "repo" installed = tmp_path / "installed" installed.mkdir() self._make_skill(repo / "skills", "cat/skill-a") report = sa.run_audit(repo, installed) assert report.has_drift assert len(report.by_status("MISSING")) == 1 def test_detects_extra_skill(self, tmp_path): repo = tmp_path / "repo" (repo / "skills").mkdir(parents=True) installed = tmp_path / "installed" self._make_skill(installed, "cat/skill-a") report = sa.run_audit(repo, installed) assert report.has_drift assert len(report.by_status("EXTRA")) == 1 def test_detects_outdated_skill(self, tmp_path): repo = tmp_path / "repo" installed = tmp_path / "installed" self._make_skill(repo / "skills", "cat/skill-a", "# Repo version") self._make_skill(installed, "cat/skill-a", "# Installed version") report = sa.run_audit(repo, installed) assert report.has_drift assert len(report.by_status("OUTDATED")) == 1 def test_fix_copies_missing_skills(self, tmp_path): repo = tmp_path / "repo" installed = tmp_path / "installed" installed.mkdir() self._make_skill(repo / "skills", "cat/skill-a", "# content") report = sa.run_audit(repo, installed) assert len(report.by_status("MISSING")) == 1 sa.apply_fix(report) report2 = sa.run_audit(repo, installed) assert not report2.has_drift # --------------------------------------------------------------------------- # dependency_checker tests # --------------------------------------------------------------------------- class TestDependencyChecker: def _make_skill(self, root: Path, rel_path: str, content: str) -> None: skill_dir = root / rel_path skill_dir.mkdir(parents=True, exist_ok=True) (skill_dir / "SKILL.md").write_text(content) def test_no_deps_when_no_frontmatter(self, tmp_path): self._make_skill(tmp_path, "cat/plain", "# No frontmatter") report = dc.run_dep_check(skills_dir=tmp_path) assert report.deps == [] def test_detects_missing_binary(self, tmp_path): content = "---\nname: test\ndependencies:\n binaries: [definitely_not_a_real_binary_xyz]\n---\n" self._make_skill(tmp_path, "cat/skill", content) report = dc.run_dep_check(skills_dir=tmp_path) assert len(report.deps) == 1 assert not report.deps[0].satisfied assert report.deps[0].binary == "definitely_not_a_real_binary_xyz" def test_detects_present_binary(self, tmp_path): content = "---\nname: test\ndependencies:\n binaries: [python3]\n---\n" self._make_skill(tmp_path, "cat/skill", content) report = dc.run_dep_check(skills_dir=tmp_path) assert len(report.deps) == 1 assert report.deps[0].satisfied def test_detects_missing_env_var(self, tmp_path): content = "---\nname: test\ndependencies:\n env_vars: [DEFINITELY_NOT_SET_XYZ_123]\n---\n" self._make_skill(tmp_path, "cat/skill", content) env = {k: v for k, v in os.environ.items() if k != "DEFINITELY_NOT_SET_XYZ_123"} with mock.patch.dict(os.environ, env, clear=True): report = dc.run_dep_check(skills_dir=tmp_path) assert len(report.deps) == 1 assert not report.deps[0].satisfied def test_detects_present_env_var(self, tmp_path): content = "---\nname: test\ndependencies:\n env_vars: [MY_TEST_VAR_WIZARD]\n---\n" self._make_skill(tmp_path, "cat/skill", content) with mock.patch.dict(os.environ, {"MY_TEST_VAR_WIZARD": "set"}): report = dc.run_dep_check(skills_dir=tmp_path) assert len(report.deps) == 1 assert report.deps[0].satisfied def test_skill_filter(self, tmp_path): content = "---\nname: test\ndependencies:\n binaries: [python3]\n---\n" self._make_skill(tmp_path, "cat/skill-a", content) self._make_skill(tmp_path, "cat/skill-b", content) report = dc.run_dep_check(skills_dir=tmp_path, skill_filter="skill-a") assert len(report.deps) == 1 assert "skill-a" in report.deps[0].skill_path