This commit was merged in pull request #158.
This commit is contained in:
242
tests/test_wizard_bootstrap.py
Normal file
242
tests/test_wizard_bootstrap.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user