Add ability to pause radio connection (closes #51)

This commit is contained in:
Jack Kingsman
2026-03-11 17:17:03 -07:00
parent e993009782
commit 4e0b6a49b0
18 changed files with 371 additions and 34 deletions
+39
View File
@@ -56,6 +56,7 @@ class TestHealthFanoutStatus:
assert data["status"] == "ok"
assert data["radio_connected"] is True
assert data["radio_initializing"] is False
assert data["radio_state"] == "connected"
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
@pytest.mark.asyncio
@@ -69,6 +70,7 @@ class TestHealthFanoutStatus:
assert data["status"] == "degraded"
assert data["radio_connected"] is False
assert data["radio_initializing"] is False
assert data["radio_state"] == "disconnected"
assert data["connection_info"] is None
@pytest.mark.asyncio
@@ -87,3 +89,40 @@ class TestHealthFanoutStatus:
assert data["status"] == "degraded"
assert data["radio_connected"] is True
assert data["radio_initializing"] is True
assert data["radio_state"] == "initializing"
@pytest.mark.asyncio
async def test_health_state_paused_when_operator_disabled_connection(self, test_db):
"""Health reports paused when the operator has disabled reconnect attempts."""
with (
patch(
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
),
patch("app.routers.health.radio_manager") as mock_rm,
):
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = False
mock_rm.connection_desired = False
mock_rm.is_reconnecting = False
data = await build_health_data(False, "BLE: AA:BB:CC:DD:EE:FF")
assert data["radio_state"] == "paused"
assert data["radio_connected"] is False
@pytest.mark.asyncio
async def test_health_state_connecting_while_reconnect_in_progress(self, test_db):
"""Health reports connecting while retries are active but transport is not up yet."""
with (
patch(
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
),
patch("app.routers.health.radio_manager") as mock_rm,
):
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = False
mock_rm.connection_desired = True
mock_rm.is_reconnecting = True
data = await build_health_data(False, None)
assert data["radio_state"] == "connecting"
assert data["radio_connected"] is False
+41
View File
@@ -300,6 +300,29 @@ class TestConnectionMonitor:
rm.post_connect_setup.assert_not_called()
@pytest.mark.asyncio
async def test_monitor_does_not_reconnect_when_connection_is_paused(self):
"""Operator-paused state suppresses reconnect attempts."""
from app.radio import RadioManager
rm = RadioManager()
rm._connection_desired = False
rm.reconnect = AsyncMock()
rm.post_connect_setup = AsyncMock()
async def _sleep(_seconds: float):
raise asyncio.CancelledError()
with patch("app.radio.asyncio.sleep", side_effect=_sleep):
await rm.start_connection_monitor()
try:
await rm._reconnect_task
finally:
await rm.stop_connection_monitor()
rm.reconnect.assert_not_called()
rm.post_connect_setup.assert_not_called()
class TestReconnectLock:
"""Tests for reconnect() lock serialization — no duplicate reconnections."""
@@ -408,6 +431,24 @@ class TestReconnectLock:
assert result2 is True
assert attempt == 2
@pytest.mark.asyncio
async def test_reconnect_returns_false_when_connection_is_paused(self):
"""Reconnect should no-op when the operator has paused connection attempts."""
from app.radio import RadioManager
rm = RadioManager()
rm._connection_desired = False
rm.connect = AsyncMock()
with (
patch("app.websocket.broadcast_health"),
patch("app.websocket.broadcast_error"),
):
result = await rm.reconnect(broadcast_on_success=False)
assert result is False
rm.connect.assert_not_called()
class TestSerialDeviceProbe:
"""Tests for test_serial_device() — verifies cleanup on all exit paths."""
+19
View File
@@ -15,6 +15,7 @@ from app.routers.radio import (
RadioConfigResponse,
RadioConfigUpdate,
RadioSettings,
disconnect_radio,
get_radio_config,
reboot_radio,
reconnect_radio,
@@ -394,3 +395,21 @@ class TestRebootAndReconnect:
await reconnect_radio()
assert exc.value.status_code == 503
@pytest.mark.asyncio
async def test_disconnect_pauses_connection_attempts_and_broadcasts_health(self):
mock_rm = MagicMock()
mock_rm.pause_connection = AsyncMock()
mock_rm.connection_info = "BLE: AA:BB:CC:DD:EE:FF"
with (
patch("app.routers.radio.radio_manager", _runtime(mock_rm)),
patch("app.routers.radio.broadcast_health") as mock_broadcast,
):
result = await disconnect_radio()
assert result["status"] == "ok"
assert result["connected"] is False
assert result["paused"] is True
mock_rm.pause_connection.assert_awaited_once()
mock_broadcast.assert_called_once_with(False, "BLE: AA:BB:CC:DD:EE:FF")