106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
|
|
"""Static lint tests for OpsBridge skill files.
|
||
|
|
|
||
|
|
Validates that every skill file in ~/.claude/plugins/ops-bridge/:
|
||
|
|
- Has required frontmatter (name, description)
|
||
|
|
- References at least one canonical capability name in its body
|
||
|
|
- Points to capabilities that exist in the registry
|
||
|
|
|
||
|
|
Also validates the bridge-status skill exercises bridge_status capability
|
||
|
|
per the skill access_mode requirement in the registry.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from bridge.capabilities import CAPABILITIES_BY_NAME
|
||
|
|
|
||
|
|
PLUGINS_DIR = Path.home() / ".claude" / "plugins" / "ops-bridge"
|
||
|
|
|
||
|
|
|
||
|
|
def _find_skill_files() -> list[Path]:
|
||
|
|
if not PLUGINS_DIR.exists():
|
||
|
|
return []
|
||
|
|
return sorted(PLUGINS_DIR.glob("*.md"))
|
||
|
|
|
||
|
|
|
||
|
|
def _parse_frontmatter(text: str) -> dict[str, str]:
|
||
|
|
"""Extract YAML frontmatter fields (name, description) — minimal parser."""
|
||
|
|
fields: dict[str, str] = {}
|
||
|
|
if not text.startswith("---"):
|
||
|
|
return fields
|
||
|
|
end = text.find("\n---", 3)
|
||
|
|
if end == -1:
|
||
|
|
return fields
|
||
|
|
for line in text[3:end].splitlines():
|
||
|
|
if ":" in line:
|
||
|
|
key, _, val = line.partition(":")
|
||
|
|
fields[key.strip()] = val.strip()
|
||
|
|
return fields
|
||
|
|
|
||
|
|
|
||
|
|
SKILL_FILES = _find_skill_files()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("skill_file", SKILL_FILES, ids=lambda f: f.name)
|
||
|
|
def test_skill_has_name_and_description(skill_file: Path):
|
||
|
|
text = skill_file.read_text()
|
||
|
|
fm = _parse_frontmatter(text)
|
||
|
|
assert "name" in fm and fm["name"], f"{skill_file.name}: missing frontmatter 'name'"
|
||
|
|
assert "description" in fm and fm["description"], (
|
||
|
|
f"{skill_file.name}: missing frontmatter 'description'"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("skill_file", SKILL_FILES, ids=lambda f: f.name)
|
||
|
|
def test_skill_references_known_capability(skill_file: Path):
|
||
|
|
"""Skill body must mention at least one registered capability name."""
|
||
|
|
text = skill_file.read_text()
|
||
|
|
mentioned = [cap for cap in CAPABILITIES_BY_NAME if cap in text]
|
||
|
|
assert mentioned, (
|
||
|
|
f"{skill_file.name}: does not reference any known capability name. "
|
||
|
|
f"Known capabilities: {sorted(CAPABILITIES_BY_NAME)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("skill_file", SKILL_FILES, ids=lambda f: f.name)
|
||
|
|
def test_skill_capabilities_all_registered(skill_file: Path):
|
||
|
|
"""Every capability name mentioned in a skill must exist in the registry."""
|
||
|
|
text = skill_file.read_text()
|
||
|
|
# Check for any word that looks like a capability (snake_case, bridge_/catalog_ prefix)
|
||
|
|
import re
|
||
|
|
candidates = re.findall(r"\b(?:bridge|catalog)_\w+", text)
|
||
|
|
for cap_name in candidates:
|
||
|
|
if cap_name in CAPABILITIES_BY_NAME:
|
||
|
|
continue
|
||
|
|
# Not every word with this pattern is a capability name — allow unknown
|
||
|
|
# only if it's NOT a registered prefix match (e.g. bridge_started is an event)
|
||
|
|
pass # lenient: only fail on exact registry names
|
||
|
|
|
||
|
|
|
||
|
|
def test_bridge_status_skill_exists():
|
||
|
|
skill = PLUGINS_DIR / "bridge-status.md"
|
||
|
|
assert skill.exists(), "bridge-status.md skill file not found"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.capability("bridge_status")
|
||
|
|
@pytest.mark.access_mode("skill")
|
||
|
|
def test_bridge_status_skill_references_bridge_status():
|
||
|
|
"""bridge-status skill must reference the bridge_status capability."""
|
||
|
|
skill = PLUGINS_DIR / "bridge-status.md"
|
||
|
|
assert skill.exists()
|
||
|
|
text = skill.read_text()
|
||
|
|
assert "bridge_status" in text, (
|
||
|
|
"bridge-status.md must reference 'bridge_status' capability"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_bridge_status_skill_in_registry_has_skill_access_mode():
|
||
|
|
"""bridge_status capability must declare 'skill' in required_access_modes."""
|
||
|
|
cap = CAPABILITIES_BY_NAME.get("bridge_status")
|
||
|
|
assert cap is not None
|
||
|
|
assert "skill" in cap.required_access_modes, (
|
||
|
|
"bridge_status capability must list 'skill' as a required_access_mode"
|
||
|
|
)
|