feat: add skill-seekers doctor health check command (#316)
8 diagnostic checks: Python version (3.10+), package install, git, 14 core deps, 10 optional deps, API keys, MCP server, output dir. Each check reports pass/warn/fail with --verbose for extra detail. Exit code 0 if no critical failures, 1 otherwise. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **`skill-seekers doctor` command** — 8 diagnostic checks (Python version, package install, git, core/optional deps, API keys, MCP server, output dir) with pass/warn/fail status and `--verbose` flag (#316)
|
||||||
- **Prompt injection check workflow** — bundled `prompt-injection-check` workflow scans scraped content for injection patterns (role assumption, instruction overrides, delimiter injection, hidden instructions). Added as first stage in `default` and `security-focus` workflows. Flags suspicious content without removing it (#324)
|
- **Prompt injection check workflow** — bundled `prompt-injection-check` workflow scans scraped content for injection patterns (role assumption, instruction overrides, delimiter injection, hidden instructions). Added as first stage in `default` and `security-focus` workflows. Flags suspicious content without removing it (#324)
|
||||||
- **6 behavioral UML diagrams** — 3 sequence (create pipeline, GitHub+C3.x flow, MCP invocation), 2 activity (source detection, enhancement pipeline), 1 component (runtime dependencies with interface contracts)
|
- **6 behavioral UML diagrams** — 3 sequence (create pipeline, GitHub+C3.x flow, MCP invocation), 2 activity (source detection, enhancement pipeline), 1 component (runtime dependencies with interface contracts)
|
||||||
|
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ skill-seekers = "skill_seekers.cli.main:main"
|
|||||||
|
|
||||||
# Individual tool entry points
|
# Individual tool entry points
|
||||||
skill-seekers-create = "skill_seekers.cli.create_command:main" # NEW: Unified create command
|
skill-seekers-create = "skill_seekers.cli.create_command:main" # NEW: Unified create command
|
||||||
|
skill-seekers-doctor = "skill_seekers.cli.doctor:main"
|
||||||
skill-seekers-config = "skill_seekers.cli.config_command:main"
|
skill-seekers-config = "skill_seekers.cli.config_command:main"
|
||||||
skill-seekers-resume = "skill_seekers.cli.resume_command:main"
|
skill-seekers-resume = "skill_seekers.cli.resume_command:main"
|
||||||
skill-seekers-scrape = "skill_seekers.cli.doc_scraper:main"
|
skill-seekers-scrape = "skill_seekers.cli.doc_scraper:main"
|
||||||
|
|||||||
303
src/skill_seekers/cli/doctor.py
Normal file
303
src/skill_seekers/cli/doctor.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
"""
|
||||||
|
skill-seekers doctor — Environment health check command.
|
||||||
|
|
||||||
|
Runs 8 quick offline checks and prints a diagnostic summary,
|
||||||
|
similar to `brew doctor` or `flutter doctor`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CheckResult:
|
||||||
|
"""Result of a single health check."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
status: Literal["pass", "warn", "fail"]
|
||||||
|
detail: str
|
||||||
|
critical: bool = False
|
||||||
|
verbose_detail: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ── Core dependency import names (from pyproject.toml dependencies) ──────────
|
||||||
|
CORE_DEPS = {
|
||||||
|
"requests": "requests",
|
||||||
|
"beautifulsoup4": "bs4",
|
||||||
|
"PyGithub": "github",
|
||||||
|
"GitPython": "git",
|
||||||
|
"httpx": "httpx",
|
||||||
|
"anthropic": "anthropic",
|
||||||
|
"PyMuPDF": "fitz",
|
||||||
|
"Pillow": "PIL",
|
||||||
|
"pydantic": "pydantic",
|
||||||
|
"pydantic-settings": "pydantic_settings",
|
||||||
|
"python-dotenv": "dotenv",
|
||||||
|
"jsonschema": "jsonschema",
|
||||||
|
"click": "click",
|
||||||
|
"Pygments": "pygments",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Optional dependency import names ─────────────────────────────────────────
|
||||||
|
OPTIONAL_DEPS = {
|
||||||
|
"mammoth": "mammoth",
|
||||||
|
"ebooklib": "ebooklib",
|
||||||
|
"yt-dlp": "yt_dlp",
|
||||||
|
"mcp": "mcp",
|
||||||
|
"google-generativeai": "google.generativeai",
|
||||||
|
"openai": "openai",
|
||||||
|
"chromadb": "chromadb",
|
||||||
|
"weaviate-client": "weaviate",
|
||||||
|
"nbformat": "nbformat",
|
||||||
|
"feedparser": "feedparser",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── API keys to check ────────────────────────────────────────────────────────
|
||||||
|
API_KEYS = ["ANTHROPIC_API_KEY", "GITHUB_TOKEN", "GOOGLE_API_KEY", "OPENAI_API_KEY"]
|
||||||
|
|
||||||
|
|
||||||
|
def _try_import(module_name: str) -> tuple[bool, str]:
|
||||||
|
"""Try to import a module and return (success, version_or_error)."""
|
||||||
|
try:
|
||||||
|
mod = __import__(module_name.split(".")[0])
|
||||||
|
version = getattr(mod, "__version__", getattr(mod, "VERSION", "installed"))
|
||||||
|
return True, str(version)
|
||||||
|
except Exception as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def check_python_version() -> CheckResult:
|
||||||
|
"""Check 1: Python version >= 3.10."""
|
||||||
|
v = sys.version_info
|
||||||
|
version_str = f"{v.major}.{v.minor}.{v.micro}"
|
||||||
|
if v >= (3, 10):
|
||||||
|
return CheckResult("Python version", "pass", version_str, critical=True)
|
||||||
|
return CheckResult("Python version", "fail", f"{version_str} (requires 3.10+)", critical=True)
|
||||||
|
|
||||||
|
|
||||||
|
def check_package_installed() -> CheckResult:
|
||||||
|
"""Check 2: skill-seekers package importable with version."""
|
||||||
|
try:
|
||||||
|
from skill_seekers._version import __version__
|
||||||
|
|
||||||
|
return CheckResult("skill-seekers installed", "pass", f"v{__version__}", critical=True)
|
||||||
|
except ImportError:
|
||||||
|
return CheckResult(
|
||||||
|
"skill-seekers installed", "fail", "Cannot import skill_seekers", critical=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_git() -> CheckResult:
|
||||||
|
"""Check 3: git available in PATH."""
|
||||||
|
git_path = shutil.which("git")
|
||||||
|
if not git_path:
|
||||||
|
return CheckResult("Git", "warn", "git not found in PATH")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["git", "--version"], capture_output=True, text=True, timeout=5)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
return CheckResult("Git", "pass", version, verbose_detail=f"Path: {git_path}")
|
||||||
|
except Exception as e:
|
||||||
|
return CheckResult("Git", "warn", f"git found but error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_core_deps() -> CheckResult:
|
||||||
|
"""Check 4: Core dependencies importable."""
|
||||||
|
found = []
|
||||||
|
missing = []
|
||||||
|
details = []
|
||||||
|
for pkg_name, import_name in CORE_DEPS.items():
|
||||||
|
ok, info = _try_import(import_name)
|
||||||
|
if ok:
|
||||||
|
found.append(pkg_name)
|
||||||
|
details.append(f" {pkg_name}: {info}")
|
||||||
|
else:
|
||||||
|
missing.append(pkg_name)
|
||||||
|
details.append(f" {pkg_name}: MISSING")
|
||||||
|
|
||||||
|
if not missing:
|
||||||
|
return CheckResult(
|
||||||
|
"Core dependencies",
|
||||||
|
"pass",
|
||||||
|
f"All {len(found)} found",
|
||||||
|
critical=True,
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
"Core dependencies",
|
||||||
|
"fail",
|
||||||
|
f"{len(missing)} missing: {', '.join(missing)}",
|
||||||
|
critical=True,
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_optional_deps() -> CheckResult:
|
||||||
|
"""Check 5: Optional dependencies status."""
|
||||||
|
found = []
|
||||||
|
missing = []
|
||||||
|
details = []
|
||||||
|
for pkg_name, import_name in OPTIONAL_DEPS.items():
|
||||||
|
ok, info = _try_import(import_name)
|
||||||
|
if ok:
|
||||||
|
found.append(pkg_name)
|
||||||
|
details.append(f" {pkg_name}: {info}")
|
||||||
|
else:
|
||||||
|
missing.append(pkg_name)
|
||||||
|
details.append(f" {pkg_name}: not installed")
|
||||||
|
|
||||||
|
total = len(OPTIONAL_DEPS)
|
||||||
|
if not missing:
|
||||||
|
return CheckResult(
|
||||||
|
"Optional dependencies",
|
||||||
|
"pass",
|
||||||
|
f"{total}/{total} installed",
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
"Optional dependencies",
|
||||||
|
"warn",
|
||||||
|
f"{len(found)}/{total} installed (not installed: {', '.join(missing)})",
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_api_keys() -> CheckResult:
|
||||||
|
"""Check 6: API keys set in environment."""
|
||||||
|
set_keys = []
|
||||||
|
missing_keys = []
|
||||||
|
details = []
|
||||||
|
for key in API_KEYS:
|
||||||
|
val = os.environ.get(key)
|
||||||
|
if val:
|
||||||
|
set_keys.append(key)
|
||||||
|
masked = val[:4] + "..." + val[-4:] if len(val) > 12 else "***"
|
||||||
|
details.append(f" {key}: {masked}")
|
||||||
|
else:
|
||||||
|
missing_keys.append(key)
|
||||||
|
details.append(f" {key}: not set")
|
||||||
|
|
||||||
|
if not missing_keys:
|
||||||
|
return CheckResult(
|
||||||
|
"API keys",
|
||||||
|
"pass",
|
||||||
|
f"All {len(API_KEYS)} set",
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
if set_keys:
|
||||||
|
return CheckResult(
|
||||||
|
"API keys",
|
||||||
|
"warn",
|
||||||
|
f"{len(set_keys)} set ({', '.join(missing_keys)} not set)",
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
return CheckResult(
|
||||||
|
"API keys",
|
||||||
|
"warn",
|
||||||
|
"None set (enhancement features will use LOCAL mode)",
|
||||||
|
verbose_detail="\n".join(details),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_mcp_server() -> CheckResult:
|
||||||
|
"""Check 7: MCP server module importable."""
|
||||||
|
try:
|
||||||
|
from skill_seekers.mcp import server_fastmcp # noqa: F401
|
||||||
|
|
||||||
|
return CheckResult("MCP server", "pass", "Importable")
|
||||||
|
except ImportError as e:
|
||||||
|
return CheckResult("MCP server", "warn", f"Not available ({e})")
|
||||||
|
except Exception as e:
|
||||||
|
return CheckResult("MCP server", "warn", f"Import error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_output_directory() -> CheckResult:
|
||||||
|
"""Check 8: Current directory is writable."""
|
||||||
|
cwd = os.getcwd()
|
||||||
|
if os.access(cwd, os.W_OK):
|
||||||
|
return CheckResult("Output directory", "pass", f"{cwd} (writable)", critical=True)
|
||||||
|
return CheckResult("Output directory", "fail", f"{cwd} (NOT writable)", critical=True)
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_checks() -> list[CheckResult]:
|
||||||
|
"""Run all 8 health checks and return results."""
|
||||||
|
return [
|
||||||
|
check_python_version(),
|
||||||
|
check_package_installed(),
|
||||||
|
check_git(),
|
||||||
|
check_core_deps(),
|
||||||
|
check_optional_deps(),
|
||||||
|
check_api_keys(),
|
||||||
|
check_mcp_server(),
|
||||||
|
check_output_directory(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_ICONS = {"pass": "\u2705", "warn": "\u26a0\ufe0f ", "fail": "\u274c"}
|
||||||
|
|
||||||
|
|
||||||
|
def print_report(results: list[CheckResult], verbose: bool = False) -> int:
|
||||||
|
"""Print formatted report and return exit code."""
|
||||||
|
try:
|
||||||
|
from skill_seekers._version import __version__
|
||||||
|
|
||||||
|
version = __version__
|
||||||
|
except ImportError:
|
||||||
|
version = "unknown"
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 50)
|
||||||
|
print(f" Skill Seekers Doctor (v{version})")
|
||||||
|
print("=" * 50)
|
||||||
|
print()
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
icon = STATUS_ICONS[r.status]
|
||||||
|
print(f" {icon} {r.name} — {r.detail}")
|
||||||
|
if verbose and r.verbose_detail:
|
||||||
|
for line in r.verbose_detail.split("\n"):
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
|
passed = sum(1 for r in results if r.status == "pass")
|
||||||
|
warnings = sum(1 for r in results if r.status == "warn")
|
||||||
|
errors = sum(1 for r in results if r.status == "fail")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("-" * 50)
|
||||||
|
print(f" {passed} passed, {warnings} warnings, {errors} errors")
|
||||||
|
|
||||||
|
if errors == 0:
|
||||||
|
if warnings > 0:
|
||||||
|
print(" All critical checks passed with some warnings.")
|
||||||
|
else:
|
||||||
|
print(" All checks passed!")
|
||||||
|
else:
|
||||||
|
print(" Some critical checks failed. Fix errors above.")
|
||||||
|
|
||||||
|
print()
|
||||||
|
return 1 if errors > 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Entry point for doctor command."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="skill-seekers doctor",
|
||||||
|
description="Check environment health and dependencies",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose", "-v", action="store_true", help="Show detailed diagnostic info"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
results = run_all_checks()
|
||||||
|
return print_report(results, verbose=args.verbose)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -56,6 +56,7 @@ from skill_seekers.cli import __version__
|
|||||||
# Command module mapping (command name -> module path)
|
# Command module mapping (command name -> module path)
|
||||||
COMMAND_MODULES = {
|
COMMAND_MODULES = {
|
||||||
"create": "skill_seekers.cli.create_command", # NEW: Unified create command
|
"create": "skill_seekers.cli.create_command", # NEW: Unified create command
|
||||||
|
"doctor": "skill_seekers.cli.doctor",
|
||||||
"config": "skill_seekers.cli.config_command",
|
"config": "skill_seekers.cli.config_command",
|
||||||
"scrape": "skill_seekers.cli.doc_scraper",
|
"scrape": "skill_seekers.cli.doc_scraper",
|
||||||
"github": "skill_seekers.cli.github_scraper",
|
"github": "skill_seekers.cli.github_scraper",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from .multilang_parser import MultilangParser
|
|||||||
from .quality_parser import QualityParser
|
from .quality_parser import QualityParser
|
||||||
from .workflows_parser import WorkflowsParser
|
from .workflows_parser import WorkflowsParser
|
||||||
from .sync_config_parser import SyncConfigParser
|
from .sync_config_parser import SyncConfigParser
|
||||||
|
from .doctor_parser import DoctorParser
|
||||||
|
|
||||||
# New source type parsers (v3.2.0+)
|
# New source type parsers (v3.2.0+)
|
||||||
from .jupyter_parser import JupyterParser
|
from .jupyter_parser import JupyterParser
|
||||||
@@ -48,6 +49,7 @@ from .chat_parser import ChatParser
|
|||||||
# Registry of all parsers (in order of usage frequency)
|
# Registry of all parsers (in order of usage frequency)
|
||||||
PARSERS = [
|
PARSERS = [
|
||||||
CreateParser(), # NEW: Unified create command (placed first for prominence)
|
CreateParser(), # NEW: Unified create command (placed first for prominence)
|
||||||
|
DoctorParser(),
|
||||||
ConfigParser(),
|
ConfigParser(),
|
||||||
ScrapeParser(),
|
ScrapeParser(),
|
||||||
GitHubParser(),
|
GitHubParser(),
|
||||||
|
|||||||
25
src/skill_seekers/cli/parsers/doctor_parser.py
Normal file
25
src/skill_seekers/cli/parsers/doctor_parser.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Doctor subcommand parser."""
|
||||||
|
|
||||||
|
from .base import SubcommandParser
|
||||||
|
|
||||||
|
|
||||||
|
class DoctorParser(SubcommandParser):
|
||||||
|
"""Parser for doctor subcommand."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "doctor"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help(self) -> str:
|
||||||
|
return "Check environment health and dependencies"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return "Run diagnostic checks on Python version, dependencies, API keys, and more"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add doctor-specific arguments."""
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose", "-v", action="store_true", help="Show detailed diagnostic info"
|
||||||
|
)
|
||||||
164
tests/test_doctor.py
Normal file
164
tests/test_doctor.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""Tests for skill-seekers doctor command (#316)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from skill_seekers.cli.doctor import (
|
||||||
|
CheckResult,
|
||||||
|
check_api_keys,
|
||||||
|
check_core_deps,
|
||||||
|
check_git,
|
||||||
|
check_mcp_server,
|
||||||
|
check_optional_deps,
|
||||||
|
check_output_directory,
|
||||||
|
check_package_installed,
|
||||||
|
check_python_version,
|
||||||
|
print_report,
|
||||||
|
run_all_checks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckPythonVersion:
|
||||||
|
def test_passes_on_current_python(self):
|
||||||
|
result = check_python_version()
|
||||||
|
assert result.status == "pass"
|
||||||
|
assert result.critical is True
|
||||||
|
|
||||||
|
def test_detail_contains_version(self):
|
||||||
|
result = check_python_version()
|
||||||
|
assert "." in result.detail # e.g. "3.14.3"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckPackageInstalled:
|
||||||
|
def test_passes_when_installed(self):
|
||||||
|
result = check_package_installed()
|
||||||
|
assert result.status == "pass"
|
||||||
|
assert result.detail.startswith("v")
|
||||||
|
|
||||||
|
def test_fails_when_import_broken(self):
|
||||||
|
with (
|
||||||
|
patch.dict("sys.modules", {"skill_seekers._version": None}),
|
||||||
|
patch("builtins.__import__", side_effect=ImportError("mocked")),
|
||||||
|
):
|
||||||
|
result = check_package_installed()
|
||||||
|
assert result.status == "fail"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckGit:
|
||||||
|
def test_passes_when_git_available(self):
|
||||||
|
result = check_git()
|
||||||
|
# Most CI/dev environments have git
|
||||||
|
assert result.status in ("pass", "warn")
|
||||||
|
|
||||||
|
def test_warns_when_git_missing(self):
|
||||||
|
with patch("skill_seekers.cli.doctor.shutil.which", return_value=None):
|
||||||
|
result = check_git()
|
||||||
|
assert result.status == "warn"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckCoreDeps:
|
||||||
|
def test_passes_in_normal_environment(self):
|
||||||
|
result = check_core_deps()
|
||||||
|
assert result.status == "pass"
|
||||||
|
assert result.critical is True
|
||||||
|
|
||||||
|
def test_detail_shows_count(self):
|
||||||
|
result = check_core_deps()
|
||||||
|
assert "found" in result.detail.lower() or "missing" in result.detail.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckOptionalDeps:
|
||||||
|
def test_returns_result(self):
|
||||||
|
result = check_optional_deps()
|
||||||
|
assert result.status in ("pass", "warn")
|
||||||
|
assert "/" in result.detail # e.g. "7/10 installed"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckApiKeys:
|
||||||
|
def test_warns_when_no_keys(self):
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
result = check_api_keys()
|
||||||
|
assert result.status == "warn"
|
||||||
|
|
||||||
|
def test_passes_when_all_set(self):
|
||||||
|
env = {
|
||||||
|
"ANTHROPIC_API_KEY": "sk-ant-test123456789",
|
||||||
|
"GITHUB_TOKEN": "ghp_test123456789",
|
||||||
|
"GOOGLE_API_KEY": "AIza_test123456789",
|
||||||
|
"OPENAI_API_KEY": "sk-test123456789",
|
||||||
|
}
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
result = check_api_keys()
|
||||||
|
assert result.status == "pass"
|
||||||
|
|
||||||
|
def test_partial_keys_warns(self):
|
||||||
|
env = {"ANTHROPIC_API_KEY": "sk-ant-test123456789"}
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
result = check_api_keys()
|
||||||
|
assert result.status == "warn"
|
||||||
|
assert "1 set" in result.detail
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckMcpServer:
|
||||||
|
def test_returns_result(self):
|
||||||
|
result = check_mcp_server()
|
||||||
|
assert result.status in ("pass", "warn")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckOutputDirectory:
|
||||||
|
def test_passes_in_writable_dir(self):
|
||||||
|
result = check_output_directory()
|
||||||
|
assert result.status == "pass"
|
||||||
|
assert result.critical is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunAllChecks:
|
||||||
|
def test_returns_8_results(self):
|
||||||
|
results = run_all_checks()
|
||||||
|
assert len(results) == 8
|
||||||
|
|
||||||
|
def test_all_have_name_and_status(self):
|
||||||
|
results = run_all_checks()
|
||||||
|
for r in results:
|
||||||
|
assert isinstance(r, CheckResult)
|
||||||
|
assert r.name
|
||||||
|
assert r.status in ("pass", "warn", "fail")
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrintReport:
|
||||||
|
def test_returns_0_when_no_failures(self, capsys):
|
||||||
|
results = [
|
||||||
|
CheckResult("Test1", "pass", "ok", critical=True),
|
||||||
|
CheckResult("Test2", "warn", "meh"),
|
||||||
|
]
|
||||||
|
code = print_report(results)
|
||||||
|
assert code == 0
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "1 passed" in captured.out
|
||||||
|
assert "1 warnings" in captured.out
|
||||||
|
|
||||||
|
def test_returns_1_when_critical_failure(self, capsys):
|
||||||
|
results = [
|
||||||
|
CheckResult("Test1", "pass", "ok"),
|
||||||
|
CheckResult("Test2", "fail", "broken", critical=True),
|
||||||
|
]
|
||||||
|
code = print_report(results)
|
||||||
|
assert code == 1
|
||||||
|
|
||||||
|
def test_verbose_shows_detail(self, capsys):
|
||||||
|
results = [
|
||||||
|
CheckResult("Test1", "pass", "ok", verbose_detail=" extra: info"),
|
||||||
|
]
|
||||||
|
print_report(results, verbose=True)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "extra: info" in captured.out
|
||||||
|
|
||||||
|
def test_no_verbose_hides_detail(self, capsys):
|
||||||
|
results = [
|
||||||
|
CheckResult("Test1", "pass", "ok", verbose_detail=" secret: hidden"),
|
||||||
|
]
|
||||||
|
print_report(results, verbose=False)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "secret: hidden" not in captured.out
|
||||||
Reference in New Issue
Block a user