Files
hermes-agent/tests/test_wizard_bootstrap.py

243 lines
9.1 KiB
Python
Raw Permalink Normal View History

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