generated from coulomb/repo-seed
feat(BRIDGE-WP-0003): MCP server, /bridge-status skill, cross-mode coverage enforcement
Implements the full BRIDGE-WP-0003 workplan: 188 tests passing, 0 lint errors. ## What's added **Capability registry** (`src/bridge/capabilities.py`): - 10 capabilities with required_access_modes (cli/mcp/skill) - Single source of truth for what OpsBridge does and where **MCP server** (`src/bridge/mcp_server/server.py`): - 10 FastMCP tools: bridge_up/down/restart/status/logs + 5 catalog_* tools - 3 resources: bridge://status, catalog://domains, catalog://targets - `.mcp.json` for project-scope auto-registration - `scripts/register_mcp.py` for user-scope machine-global registration **Skill** (`~/.claude/plugins/ops-bridge/bridge-status.md`): - /bridge-status: health table with emoji indicators + remediation advice **Cross-mode test coverage enforcement**: - `tests/conftest.py`: capability/access_mode marks + collect_capability_coverage() - `tests/test_mcp.py`: 31 FastMCP in-process client tests (Client(mcp) pattern) - `tests/test_skill.py`: static skill lint against capability registry - `tests/test_coverage_completeness.py`: meta-test that fails if any required (capability × mode) pair lacks a test; also validates CLI commands and MCP tools are registered in the capability registry **ADR** (`architecture/adr-001-cross-mode-capability-registry.md`): - Documents the registry pattern and FastMCP 3.x testing approach Key implementation note: FastMCP 3.x in-process results are in result.content[0].text (JSON string), not result.data directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
157
tests/test_coverage_completeness.py
Normal file
157
tests/test_coverage_completeness.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Cross-mode capability coverage meta-test.
|
||||
|
||||
Enforces that every capability in the registry has at least one test
|
||||
marked with @pytest.mark.capability(name) and @pytest.mark.access_mode(mode)
|
||||
for each of its required_access_modes.
|
||||
|
||||
The test discovers coverage by walking all collected test items, so it will
|
||||
only pass when the full test suite is collected (i.e. run without -k filters
|
||||
that exclude capability-marked tests).
|
||||
|
||||
Also validates the registry itself is self-consistent.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from bridge.capabilities import CAPABILITIES, CAPABILITIES_BY_NAME
|
||||
from tests.conftest import collect_capability_coverage
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry self-consistency
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_registry_has_capabilities():
|
||||
"""Sanity: registry must be non-empty."""
|
||||
assert len(CAPABILITIES) > 0
|
||||
|
||||
|
||||
def test_registry_names_are_unique():
|
||||
names = [c.name for c in CAPABILITIES]
|
||||
assert len(names) == len(set(names)), "Duplicate capability names in registry"
|
||||
|
||||
|
||||
def test_registry_access_modes_are_valid():
|
||||
valid = {"cli", "mcp", "skill"}
|
||||
for cap in CAPABILITIES:
|
||||
unknown = cap.required_access_modes - valid
|
||||
assert not unknown, (
|
||||
f"Capability '{cap.name}' has unknown access modes: {unknown}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_each_capability_has_at_least_one_mode():
|
||||
for cap in CAPABILITIES:
|
||||
assert cap.required_access_modes, (
|
||||
f"Capability '{cap.name}' has no required_access_modes"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-mode coverage completeness (session-scope fixture)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def capability_coverage(request) -> set[tuple[str, str]]:
|
||||
"""Collect all (capability, access_mode) pairs from the test session."""
|
||||
return collect_capability_coverage(request.session.items)
|
||||
|
||||
|
||||
def test_all_required_modes_have_tests(capability_coverage):
|
||||
"""Every (capability, mode) pair in the registry must have a test."""
|
||||
missing: list[str] = []
|
||||
for cap in CAPABILITIES:
|
||||
for mode in sorted(cap.required_access_modes):
|
||||
if (cap.name, mode) not in capability_coverage:
|
||||
missing.append(f" {cap.name!r} × {mode!r}")
|
||||
|
||||
if missing:
|
||||
pytest.fail(
|
||||
"Missing test coverage for the following (capability, access_mode) pairs:\n"
|
||||
+ "\n".join(missing)
|
||||
+ "\n\nAdd a test with @pytest.mark.capability(<name>) and "
|
||||
"@pytest.mark.access_mode(<mode>)."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T02 — Registry completeness against CLI commands and MCP tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_registry_cli_capabilities_have_matching_commands():
|
||||
"""Every capability requiring CLI must have a corresponding CLI command.
|
||||
|
||||
Checks that the registry doesn't list CLI requirements for operations that
|
||||
don't actually exist as CLI commands. Uses the Typer app's callback names.
|
||||
"""
|
||||
from bridge.cli import app, targets_app, catalog_app
|
||||
|
||||
# Collect all CLI callback function names (canonical command identity)
|
||||
top_level = {f"bridge_{cmd.callback.__name__}" for cmd in app.registered_commands}
|
||||
# targets sub-commands: callback name "targets_show" → "catalog_show_target"
|
||||
targets_cmds = set()
|
||||
for cmd in targets_app.registered_commands:
|
||||
fn = cmd.callback.__name__
|
||||
if fn == "targets_show":
|
||||
targets_cmds.add("catalog_show_target")
|
||||
catalog_cmds = set()
|
||||
for cmd in catalog_app.registered_commands:
|
||||
fn = cmd.callback.__name__
|
||||
if fn == "catalog_list":
|
||||
catalog_cmds.add("catalog_list_domains")
|
||||
elif fn == "catalog_validate":
|
||||
catalog_cmds.add("catalog_validate")
|
||||
elif fn == "catalog_show":
|
||||
catalog_cmds.add("catalog_show_bridge")
|
||||
|
||||
# Also include catalog_list_targets (from targets_app without sub-command filter)
|
||||
# The targets app root command lists targets
|
||||
all_cli_caps = top_level | targets_cmds | catalog_cmds | {"catalog_list_targets"}
|
||||
|
||||
for cap in CAPABILITIES:
|
||||
if "cli" in cap.required_access_modes:
|
||||
assert cap.name in all_cli_caps, (
|
||||
f"Capability '{cap.name}' requires CLI coverage but no matching "
|
||||
f"CLI command was found. Either add the command or update the registry."
|
||||
)
|
||||
|
||||
|
||||
async def test_mcp_tools_in_registry():
|
||||
"""Every MCP tool name must appear as a capability in the registry."""
|
||||
from fastmcp import Client
|
||||
from bridge.mcp_server.server import mcp
|
||||
|
||||
async with Client(mcp) as c:
|
||||
tools = await c.list_tools()
|
||||
tool_names = {t.name for t in tools}
|
||||
|
||||
registered_cap_names = set(CAPABILITIES_BY_NAME)
|
||||
for name in tool_names:
|
||||
assert name in registered_cap_names, (
|
||||
f"MCP tool '{name}' is not registered as a capability. "
|
||||
f"Add it to src/bridge/capabilities.py."
|
||||
)
|
||||
|
||||
|
||||
def test_no_orphan_capability_marks(capability_coverage):
|
||||
"""Every (capability, mode) pair in the test suite must exist in the registry.
|
||||
|
||||
This prevents tests from referencing stale or misspelled capability names.
|
||||
"""
|
||||
orphans: list[str] = []
|
||||
for cap_name, mode in sorted(capability_coverage):
|
||||
if cap_name not in CAPABILITIES_BY_NAME:
|
||||
orphans.append(f" {cap_name!r} (mode={mode!r}) — not in registry")
|
||||
else:
|
||||
cap = CAPABILITIES_BY_NAME[cap_name]
|
||||
if mode not in cap.required_access_modes:
|
||||
orphans.append(
|
||||
f" {cap_name!r} × {mode!r} — mode not required for this capability"
|
||||
)
|
||||
|
||||
if orphans:
|
||||
pytest.fail(
|
||||
"Test suite references capability/mode pairs not in registry:\n"
|
||||
+ "\n".join(orphans)
|
||||
)
|
||||
Reference in New Issue
Block a user