Files
ops-bridge/tests/conftest.py
tegwick 365c0d611a 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>
2026-03-12 11:33:16 +01:00

155 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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