Files
ops-bridge/tests/test_config.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

130 lines
3.7 KiB
Python

"""Tests for config loading."""
import textwrap
import pytest
from bridge.config import ConfigError, load_config
VALID_YAML = textwrap.dedent("""\
tunnels:
state-hub-coulombcore:
host: coulombcore.local
remote_port: 18000
local_port: 8000
ssh_user: ubuntu
ssh_key: ~/.ssh/id_ops
actor: agent.claude-coulombcore
health_check:
url: http://127.0.0.1:18000/health
interval_seconds: 30
timeout_seconds: 5
reconnect:
max_attempts: 0
backoff_initial: 5
backoff_max: 60
actors:
agent.claude-coulombcore:
class: automation
description: Claude Code agent on CoulombCore
operator.bernd:
class: human
description: Bernd Worsch
""")
@pytest.fixture
def config_file(tmp_path):
f = tmp_path / "tunnels.yaml"
f.write_text(VALID_YAML)
return f
def test_load_valid_config(config_file, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(config_file))
cfg = load_config()
assert "state-hub-coulombcore" in cfg.tunnels
t = cfg.tunnels["state-hub-coulombcore"]
assert t.host == "coulombcore.local"
assert t.remote_port == 18000
assert t.local_port == 8000
assert t.ssh_user == "ubuntu"
assert t.actor == "agent.claude-coulombcore"
def test_health_check_loaded(config_file, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(config_file))
cfg = load_config()
t = cfg.tunnels["state-hub-coulombcore"]
assert t.health_check is not None
assert t.health_check.url == "http://127.0.0.1:18000/health"
assert t.health_check.interval_seconds == 30
def test_reconnect_policy_loaded(config_file, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(config_file))
cfg = load_config()
t = cfg.tunnels["state-hub-coulombcore"]
assert t.reconnect.max_attempts == 0
assert t.reconnect.backoff_initial == 5
assert t.reconnect.backoff_max == 60
def test_actors_loaded(config_file, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(config_file))
cfg = load_config()
assert "agent.claude-coulombcore" in cfg.actors
a = cfg.actors["agent.claude-coulombcore"]
assert a.actor_class == "automation"
assert "operator.bernd" in cfg.actors
def test_missing_required_field_raises(tmp_path, monkeypatch):
f = tmp_path / "bad.yaml"
f.write_text(textwrap.dedent("""\
tunnels:
broken:
remote_port: 18000
local_port: 8000
actors: {}
"""))
monkeypatch.setenv("BRIDGE_CONFIG", str(f))
with pytest.raises(ConfigError, match="host"):
load_config()
def test_invalid_yaml_raises(tmp_path, monkeypatch):
f = tmp_path / "bad.yaml"
f.write_text("tunnels: [\nnot: valid: yaml")
monkeypatch.setenv("BRIDGE_CONFIG", str(f))
with pytest.raises(ConfigError):
load_config()
def test_missing_config_file_raises(tmp_path, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(tmp_path / "nonexistent.yaml"))
with pytest.raises(ConfigError, match="not found"):
load_config()
def test_tunnel_without_health_check(tmp_path, monkeypatch):
f = tmp_path / "tunnels.yaml"
f.write_text(textwrap.dedent("""\
tunnels:
simple:
host: host.local
remote_port: 9000
local_port: 8000
ssh_user: ubuntu
ssh_key: ~/.ssh/id_rsa
actor: operator.bernd
actors:
operator.bernd:
class: human
description: Bernd
"""))
monkeypatch.setenv("BRIDGE_CONFIG", str(f))
cfg = load_config()
assert cfg.tunnels["simple"].health_check is None