Don't use a stale MC instance

This commit is contained in:
Jack Kingsman
2026-03-11 21:57:59 -07:00
parent bf53e8a4cb
commit 8c1a58b293
2 changed files with 67 additions and 5 deletions
+7 -4
View File
@@ -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
+60 -1
View File
@@ -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()