Files
ops-bridge/tests/test_cleanup.py
tegwick 10c6fdaec9 feat(restart): route reverse tunnels through stale-forward cleanup
bridge restart now means blank-slate recovery: reverse tunnels run
should_cleanup_tunnel and clear orphan remote listeners before reconnecting;
healthy forwards are left running. Local-direction tunnels keep stop/start
only. CLI and MCP report per-tunnel actions (healthy, cleaned_and_restarted,
restarted, error) and exit non-zero on cleanup failure.

Closes BRIDGE-WP-0005.
2026-06-21 20:12:13 +02:00

130 lines
4.4 KiB
Python

"""Tests for stale SSH forward cleanup."""
from __future__ import annotations
import textwrap
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from bridge.cleanup import (
CleanupAction,
build_cron_line,
cleanup_all_tunnels,
remote_forward_health_url,
should_cleanup_tunnel,
)
from bridge.cli import app
from bridge.config import load_config
from bridge.models import HealthCheckConfig, TunnelConfig
from bridge.state import StateManager
def _tunnel(**overrides) -> TunnelConfig:
base = dict(
name="state-hub-railiance01",
host="92.205.62.239",
remote_port=18000,
local_port=8000,
ssh_user="tegwick",
ssh_key="~/.ssh/id_ops",
actor="agt-claude-railiance01",
health_check=HealthCheckConfig(
url="http://127.0.0.1:8000/state/health",
timeout_seconds=5,
),
)
base.update(overrides)
return TunnelConfig(**base)
class TestRemoteForwardHealthUrl:
def test_maps_local_port_to_remote(self):
cfg = _tunnel()
assert remote_forward_health_url(cfg) == "http://127.0.0.1:18000/state/health"
def test_returns_none_for_local_tunnel(self):
cfg = _tunnel(direction="local")
assert remote_forward_health_url(cfg) is None
class TestShouldCleanupTunnel:
def test_skips_healthy_remote_forward(self, tmp_path):
cfg = _tunnel()
state_mgr = StateManager(state_dir=tmp_path)
with (
patch("bridge.cleanup.remote_port_listening", return_value=True),
patch("bridge.cleanup.probe_remote_forward", return_value=(True, "ok")),
):
needed, reason = should_cleanup_tunnel(cfg, state_mgr)
assert needed is False
def test_detects_stale_forward_when_local_ok_remote_fails(self, tmp_path):
cfg = _tunnel()
state_mgr = StateManager(state_dir=tmp_path)
with (
patch("bridge.cleanup.remote_port_listening", return_value=True),
patch("bridge.cleanup.probe_remote_forward", return_value=(False, "timeout")),
patch("bridge.cleanup.local_service_healthy", return_value=True),
patch(
"bridge.cleanup.check_tunnel",
return_value=MagicMock(ssh_process="ok", remote_port="listening"),
),
):
needed, reason = should_cleanup_tunnel(cfg, state_mgr)
assert needed is True
assert "stale forward" in reason
class TestCleanupAllTunnels:
def test_reports_cleaned_tunnel(self, tmp_path, monkeypatch):
monkeypatch.setenv("BRIDGE_CONFIG", str(tmp_path / "tunnels.yaml"))
(tmp_path / "tunnels.yaml").write_text(
textwrap.dedent(
"""\
tunnels:
state-hub-railiance01:
host: 92.205.62.239
remote_port: 18000
local_port: 8000
ssh_user: tegwick
ssh_key: ~/.ssh/id_ops
actor: agt-claude-railiance01
health_check:
url: http://127.0.0.1:8000/state/health
actors:
agt-claude-railiance01:
class: agt
"""
)
)
cfg = load_config()
state_mgr = StateManager(state_dir=tmp_path / "state")
with patch(
"bridge.cleanup.cleanup_tunnel",
return_value=CleanupAction("state-hub-railiance01", "cleaned", "cleared"),
):
report = cleanup_all_tunnels(cfg, state_mgr, restart=False)
assert report.cleaned_count == 1
assert report.actions[0].action == "cleaned"
class TestMaintenanceCli:
def test_cleanup_help(self):
runner = CliRunner()
result = runner.invoke(app, ["maintenance", "cleanup", "--help"])
assert result.exit_code == 0
assert "restart" in result.output.lower()
def test_show_cron_prints_template_when_not_installed(self):
runner = CliRunner()
with patch("bridge.cli.read_installed_cron", return_value=None):
result = runner.invoke(app, ["maintenance", "show-cron"])
assert result.exit_code == 0
assert "0 3 * * *" in result.output
def test_build_cron_line_contains_marker():
line = build_cron_line()
assert "0 3 * * *" in line
assert "maintenance cleanup --restart" in line
assert "ops-bridge: maintenance cleanup" in line