generated from coulomb/repo-seed
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>
155 lines
4.3 KiB
Python
155 lines
4.3 KiB
Python
"""Shared pytest configuration for OpsBridge tests.
|
||
|
||
Registers capability and access_mode marks, and provides the
|
||
collect_capability_coverage() helper used by the cross-mode meta-test.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import textwrap
|
||
from typing import Iterable
|
||
|
||
import pytest
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Shared fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
VALID_CONFIG = textwrap.dedent("""\
|
||
tunnels:
|
||
test-tunnel:
|
||
host: host.local
|
||
remote_port: 18000
|
||
local_port: 8000
|
||
ssh_user: ubuntu
|
||
ssh_key: ~/.ssh/id_ops
|
||
actor: operator.bernd
|
||
actors:
|
||
operator.bernd:
|
||
class: human
|
||
description: Bernd
|
||
""")
|
||
|
||
VALID_CONFIG_WITH_CATALOG = textwrap.dedent("""\
|
||
tunnels:
|
||
test-tunnel:
|
||
host: host.local
|
||
remote_port: 18000
|
||
local_port: 8000
|
||
ssh_user: ubuntu
|
||
ssh_key: ~/.ssh/id_ops
|
||
actor: operator.bernd
|
||
actors:
|
||
operator.bernd:
|
||
class: human
|
||
description: Bernd
|
||
catalog_path: {catalog_path}
|
||
""")
|
||
|
||
|
||
@pytest.fixture
|
||
def config_file(tmp_path):
|
||
f = tmp_path / "tunnels.yaml"
|
||
f.write_text(VALID_CONFIG)
|
||
return f
|
||
|
||
|
||
@pytest.fixture
|
||
def state_dir(tmp_path):
|
||
d = tmp_path / "state"
|
||
d.mkdir()
|
||
return d
|
||
|
||
|
||
@pytest.fixture
|
||
def catalog_dir(tmp_path):
|
||
"""Minimal catalog directory with one domain, target, and bridge."""
|
||
cat = tmp_path / "catalog"
|
||
domain_dir = cat / "domains" / "coulombcore"
|
||
domain_dir.mkdir(parents=True)
|
||
(domain_dir / "domain.yaml").write_text(textwrap.dedent("""\
|
||
type: domain
|
||
id: coulombcore
|
||
name: CoulombCore Infrastructure
|
||
description: Core infrastructure domain
|
||
environment: production
|
||
"""))
|
||
targets_dir = domain_dir / "targets"
|
||
targets_dir.mkdir()
|
||
(targets_dir / "state-hub.yaml").write_text(textwrap.dedent("""\
|
||
type: target
|
||
id: state-hub
|
||
domain: coulombcore
|
||
kind: service
|
||
description: Infrastructure state coordination service
|
||
reachable_via:
|
||
- state-hub-coulombcore
|
||
"""))
|
||
bridges_dir = domain_dir / "bridges"
|
||
bridges_dir.mkdir()
|
||
(bridges_dir / "state-hub-coulombcore.yaml").write_text(textwrap.dedent("""\
|
||
type: bridge
|
||
id: state-hub-coulombcore
|
||
domain: coulombcore
|
||
target: state-hub
|
||
description: Bridge to state hub
|
||
access_method: ssh-reverse
|
||
host: coulombcore.local
|
||
remote_port: 18000
|
||
local_port: 8000
|
||
ssh_user: ubuntu
|
||
ssh_key: ~/.ssh/id_ops
|
||
actor: agent.claude-coulombcore
|
||
reconnect:
|
||
max_attempts: 0
|
||
backoff_initial: 5
|
||
backoff_max: 60
|
||
"""))
|
||
actors_dir = cat / "actors"
|
||
actors_dir.mkdir()
|
||
(actors_dir / "agent.yaml").write_text(textwrap.dedent("""\
|
||
type: actor
|
||
id: agent.claude-coulombcore
|
||
class: automation
|
||
description: Claude Code agent on CoulombCore
|
||
"""))
|
||
return cat
|
||
|
||
|
||
@pytest.fixture
|
||
def config_file_with_catalog(tmp_path, catalog_dir):
|
||
f = tmp_path / "tunnels.yaml"
|
||
f.write_text(VALID_CONFIG_WITH_CATALOG.format(catalog_path=str(catalog_dir)))
|
||
return f
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Coverage collector helper
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def collect_capability_coverage(items: Iterable) -> set[tuple[str, str]]:
|
||
"""Walk pytest items and return set of (capability_name, access_mode) pairs.
|
||
|
||
Each test item is inspected for `capability` and `access_mode` markers.
|
||
A pair is added for every combination of capability × access_mode marks
|
||
found on a single item.
|
||
|
||
Args:
|
||
items: Iterable of pytest.Item objects (from session.items or similar).
|
||
|
||
Returns:
|
||
Set of (capability_name, access_mode) tuples found across all items.
|
||
"""
|
||
covered: set[tuple[str, str]] = set()
|
||
for item in items:
|
||
capabilities = [
|
||
m.args[0] for m in item.iter_markers("capability") if m.args
|
||
]
|
||
modes = [
|
||
m.args[0] for m in item.iter_markers("access_mode") if m.args
|
||
]
|
||
for cap in capabilities:
|
||
for mode in modes:
|
||
covered.add((cap, mode))
|
||
return covered
|