From 8c1a58b293f633867a90a531fb7fe9d3a2d11f76 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 11 Mar 2026 21:57:59 -0700 Subject: [PATCH] Don't use a stale MC instance --- app/services/radio_lifecycle.py | 11 +++-- tests/test_radio_lifecycle_service.py | 61 ++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/app/services/radio_lifecycle.py b/app/services/radio_lifecycle.py index 110135c..1261043 100644 --- a/app/services/radio_lifecycle.py +++ b/app/services/radio_lifecycle.py @@ -32,16 +32,19 @@ async def run_post_connect_setup(radio_manager) -> None: return radio_manager._setup_in_progress = True radio_manager._setup_complete = False - mc = radio_manager.meshcore try: - # Register event handlers (no radio I/O, just callback setup) - register_event_handlers(mc) - # Hold the operation lock for all radio I/O during setup. # This prevents user-initiated operations (send message, etc.) # from interleaving commands on the serial link. await radio_manager._acquire_operation_lock("post_connect_setup", blocking=True) try: + mc = radio_manager.meshcore + if not mc: + return + + # Register event handlers against the locked, current transport. + register_event_handlers(mc) + await export_and_store_private_key(mc) # Sync radio clock with system time diff --git a/tests/test_radio_lifecycle_service.py b/tests/test_radio_lifecycle_service.py index 6b96234..d93e472 100644 --- a/tests/test_radio_lifecycle_service.py +++ b/tests/test_radio_lifecycle_service.py @@ -2,7 +2,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from app.services.radio_lifecycle import prepare_connected_radio, reconnect_and_prepare_radio +from app.services.radio_lifecycle import ( + prepare_connected_radio, + reconnect_and_prepare_radio, + run_post_connect_setup, +) class TestPrepareConnectedRadio: @@ -82,3 +86,58 @@ class TestReconnectAndPrepareRadio: assert result is False radio_manager.post_connect_setup.assert_not_awaited() mock_broadcast.assert_not_called() + + +class TestRunPostConnectSetup: + @pytest.mark.asyncio + async def test_uses_current_meshcore_after_waiting_for_operation_lock(self): + initial_mc = MagicMock() + initial_mc.commands.send_device_query = AsyncMock(return_value=None) + initial_mc.commands.set_flood_scope = AsyncMock(return_value=None) + initial_mc._reader = MagicMock() + initial_mc._reader.handle_rx = AsyncMock() + initial_mc.start_auto_message_fetching = AsyncMock() + + replacement_mc = MagicMock() + replacement_mc.commands.send_device_query = AsyncMock(return_value=None) + replacement_mc.commands.set_flood_scope = AsyncMock(return_value=None) + replacement_mc._reader = MagicMock() + replacement_mc._reader.handle_rx = AsyncMock() + replacement_mc.start_auto_message_fetching = AsyncMock() + + radio_manager = MagicMock() + radio_manager.meshcore = initial_mc + radio_manager._setup_lock = None + radio_manager._setup_in_progress = False + radio_manager._setup_complete = False + radio_manager.path_hash_mode = 0 + radio_manager.path_hash_mode_supported = False + + async def _acquire(*args, **kwargs): + radio_manager.meshcore = replacement_mc + + radio_manager._acquire_operation_lock = AsyncMock(side_effect=_acquire) + radio_manager._release_operation_lock = MagicMock() + + with ( + patch("app.event_handlers.register_event_handlers") as mock_register_handlers, + patch("app.keystore.export_and_store_private_key", new=AsyncMock()) as mock_export_key, + patch("app.radio_sync.sync_radio_time", new=AsyncMock()) as mock_sync_time, + patch( + "app.repository.AppSettingsRepository.get", + new=AsyncMock(return_value=MagicMock(flood_scope=None)), + ), + patch("app.radio_sync.sync_and_offload_all", new=AsyncMock(return_value={"synced": 0})), + patch("app.radio_sync.send_advertisement", new=AsyncMock(return_value=False)), + patch("app.radio_sync.drain_pending_messages", new=AsyncMock(return_value=0)), + patch("app.radio_sync.start_periodic_sync"), + patch("app.radio_sync.start_periodic_advert"), + patch("app.radio_sync.start_message_polling"), + ): + await run_post_connect_setup(radio_manager) + + mock_register_handlers.assert_called_once_with(replacement_mc) + mock_export_key.assert_awaited_once_with(replacement_mc) + mock_sync_time.assert_awaited_once_with(replacement_mc) + replacement_mc.start_auto_message_fetching.assert_awaited_once() + initial_mc.start_auto_message_fetching.assert_not_called()