Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions packages/claude-code-plugin/hooks/lib/health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import os
import sqlite3
import stat
import sys
from typing import Dict, List, Optional

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from runtime_mode import detect_runtime_mode

HOOK_FILES = [
"session-start.py",
"pre-tool-use.py",
Expand All @@ -26,7 +30,7 @@ def _result(check: str, status: str, message: str) -> Dict[str, str]:


class HealthChecker:
"""Runs 7 diagnostic checks on the CodingBuddy plugin environment."""
"""Runs 10 diagnostic checks on the CodingBuddy plugin environment."""

def __init__(
self,
Expand Down Expand Up @@ -165,11 +169,65 @@ def check_events_dir(self) -> Dict[str, str]:
return _result("events_dir", "PASS", "events/ directory exists")
return _result("events_dir", "WARN", "events/ directory not found")

# ------------------------------------------------------------------
# Check 8: MCP connection
# ------------------------------------------------------------------
def check_mcp_connection(self) -> Dict[str, str]:
"""Check if CodingBuddy MCP server is configured in mcp.json."""
mcp_json = os.path.join(self._claude_dir, "mcp.json")
if not os.path.isfile(mcp_json):
return _result("mcp_connection", "WARN", "mcp.json not found — standalone mode")
try:
with open(mcp_json, "r", encoding="utf-8") as f:
data = json.load(f)
servers = data.get("mcpServers", {})
for key in servers:
if "codingbuddy" in key.lower():
return _result("mcp_connection", "PASS", "MCP configured (codingbuddy entry found)")
return _result("mcp_connection", "WARN", "MCP not configured — standalone mode active")
except (json.JSONDecodeError, OSError):
return _result("mcp_connection", "WARN", "mcp.json unreadable — standalone mode")

# ------------------------------------------------------------------
# Check 9: runtime mode
# ------------------------------------------------------------------
def check_runtime_mode(self) -> Dict[str, str]:
"""Detect current runtime mode (mcp or standalone)."""
mode = detect_runtime_mode(self._home_dir)
return _result("runtime_mode", "PASS", f"Runtime: {mode}")

# ------------------------------------------------------------------
# Check 10: standalone readiness
# ------------------------------------------------------------------
def check_standalone_readiness(self) -> Dict[str, str]:
"""Check if standalone mode prerequisites are met."""
issues = []
# Check .ai-rules existence
rules_dir = os.path.join(self._project_dir, ".ai-rules")
if not os.path.isdir(rules_dir):
# Also check packages path
pkg_rules = os.path.join(self._plugin_root, "..", "rules", ".ai-rules")
if not os.path.isdir(os.path.normpath(pkg_rules)):
issues.append(".ai-rules/ not found")
# Check ModeEngine importable
try:
from mode_engine import ModeEngine # noqa: F401
except ImportError:
issues.append("ModeEngine not importable")
# Check UserPromptSubmit registered
settings_result = self.check_settings_hook()
if settings_result["status"] != "PASS":
issues.append("UserPromptSubmit hook not registered")

if not issues:
return _result("standalone_readiness", "PASS", "Standalone mode ready")
return _result("standalone_readiness", "WARN", f"Standalone not ready: {', '.join(issues)}")

# ------------------------------------------------------------------
# Aggregate
# ------------------------------------------------------------------
def run_all(self) -> List[Dict[str, str]]:
"""Run all 7 diagnostic checks and return results."""
"""Run all 10 diagnostic checks and return results."""
return [
self.check_hooks_json(),
self.check_hook_files(),
Expand All @@ -178,6 +236,9 @@ def run_all(self) -> List[Dict[str, str]]:
self.check_config(),
self.check_secrets_permissions(),
self.check_events_dir(),
self.check_mcp_connection(),
self.check_runtime_mode(),
self.check_standalone_readiness(),
]

@staticmethod
Expand Down
66 changes: 62 additions & 4 deletions packages/claude-code-plugin/tests/test_health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,72 @@ def test_events_dir_missing(self, env):
assert result["status"] == "WARN"


class TestCheckMcpConnection:
"""Check 8: MCP connection detection."""

def test_mcp_configured(self, env):
mcp_json = env / ".claude" / "mcp.json"
mcp_json.write_text(json.dumps({
"mcpServers": {"codingbuddy": {"command": "codingbuddy", "args": ["mcp"]}}
}))
checker = _make_checker(env)
result = checker.check_mcp_connection()
assert result["status"] == "PASS"
assert "codingbuddy entry found" in result["message"]

def test_mcp_not_configured(self, env):
mcp_json = env / ".claude" / "mcp.json"
mcp_json.write_text(json.dumps({"mcpServers": {"other-server": {}}}))
checker = _make_checker(env)
result = checker.check_mcp_connection()
assert result["status"] == "WARN"
assert "standalone" in result["message"]

def test_mcp_json_missing(self, env):
checker = _make_checker(env)
result = checker.check_mcp_connection()
assert result["status"] == "WARN"
assert "not found" in result["message"]


class TestCheckRuntimeMode:
"""Check 9: runtime mode detection."""

def test_returns_valid_mode(self, env):
checker = _make_checker(env)
result = checker.check_runtime_mode()
assert result["status"] == "PASS"
assert result["check"] == "runtime_mode"
assert "Runtime:" in result["message"]
assert result["message"] in ("Runtime: mcp", "Runtime: standalone")


class TestCheckStandaloneReadiness:
"""Check 10: standalone readiness."""

def test_ready_with_all_requirements(self, env):
# Create .ai-rules directory
(env / ".ai-rules").mkdir()
checker = _make_checker(env)
result = checker.check_standalone_readiness()
assert result["check"] == "standalone_readiness"
# ModeEngine may not be importable in test env, so just check it runs
assert result["status"] in ("PASS", "WARN")

def test_missing_ai_rules(self, env):
checker = _make_checker(env)
result = checker.check_standalone_readiness()
assert result["status"] == "WARN"
assert ".ai-rules/" in result["message"]


class TestRunAll:
"""run_all() returns all 7 check results."""
"""run_all() returns all 10 check results."""

def test_returns_7_results(self, env):
def test_returns_10_results(self, env):
checker = _make_checker(env)
results = checker.run_all()
assert len(results) == 7
assert all(r["status"] == "PASS" for r in results)
assert len(results) == 10

def test_each_check_has_required_keys(self, env):
checker = _make_checker(env)
Expand Down
Loading