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.
This commit is contained in:
2026-06-21 20:12:13 +02:00
parent 8c11acc00c
commit 10c6fdaec9
8 changed files with 220 additions and 60 deletions

View File

@@ -237,22 +237,22 @@ class TestMcpBridgeDown:
class TestMcpBridgeRestart:
@pytest.mark.capability("bridge_restart")
@pytest.mark.access_mode("mcp")
async def test_bridge_restart_calls_stop_then_start(self, env_simple):
with patch("bridge.manager.TunnelManager") as mock_cls:
mock_mgr = MagicMock()
call_order = []
mock_mgr.stop.side_effect = lambda: call_order.append("stop")
mock_mgr.start.side_effect = lambda: call_order.append("start")
mock_cls.return_value = mock_mgr
async def test_bridge_restart_delegates_to_cleanup(self, env_simple):
from bridge.cleanup import CleanupAction
with patch("bridge.cleanup.restart_tunnel") as mock_restart:
mock_restart.return_value = CleanupAction(
"test-tunnel", "healthy", "remote forward healthy"
)
from fastmcp import Client
async with Client(mcp) as c:
result = await c.call_tool("bridge_restart", {"tunnel": "test-tunnel"})
data = _data(result)
assert "restarted" in data
assert "test-tunnel" in data["restarted"]
assert call_order == ["stop", "start"]
assert data["actions"][0]["tunnel"] == "test-tunnel"
assert data["actions"][0]["action"] == "healthy"
mock_restart.assert_called_once()
async def test_bridge_restart_unknown_tunnel(self, env_simple):
from fastmcp import Client