243 lines
9.1 KiB
Python
243 lines
9.1 KiB
Python
"""
|
|
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
|