From 132bb8a163d07fc8553819a3cafdd6a21f348fa8 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Sun, 5 Jul 2026 00:45:30 +0800 Subject: [PATCH] fix(yuanbao): restore active singleton after WS reconnect _do_reconnect() succeeded but never called YuanbaoAdapter.set_active(adapter), leaving get_active() permanently returning None after any WS disconnect/reconnect cycle. This caused cron delivery to silently fail because _send_yuanbao() checks get_active_adapter() and gives up immediately when it returns None. Fix: call set_active(adapter) after successful reconnect, matching the pattern in connect(). Fixes #58363 --- gateway/platforms/yuanbao.py | 1 + tests/test_yuanbao_reconnect_set_active.py | 106 +++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/test_yuanbao_reconnect_set_active.py diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index a26f3a43bd2..c6c410a0a43 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -3868,6 +3868,7 @@ class ConnectionManager: "[%s] Reconnected on attempt %d. connectId=%s", adapter.name, attempt + 1, self._connect_id, ) + YuanbaoAdapter.set_active(adapter) return True except asyncio.TimeoutError: diff --git a/tests/test_yuanbao_reconnect_set_active.py b/tests/test_yuanbao_reconnect_set_active.py new file mode 100644 index 00000000000..5483dfc5edc --- /dev/null +++ b/tests/test_yuanbao_reconnect_set_active.py @@ -0,0 +1,106 @@ +"""test_yuanbao_reconnect_set_active.py - Verify _do_reconnect restores the active singleton. + +Regression test for #58363: after a WS disconnect/reconnect cycle, +``get_active_adapter()`` must return the live adapter (not ``None``). +The original ``_do_reconnect()`` succeeded but never called +``YuanbaoAdapter.set_active()``, leaving the singleton permanently +``None`` until a full gateway restart. +""" + +import sys +import os +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch + +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if _REPO_ROOT not in sys.path: + sys.path.insert(0, _REPO_ROOT) + +import pytest +from gateway.platforms.yuanbao import ( + YuanbaoAdapter, + ConnectionManager, + get_active_adapter, +) + + +def _make_adapter(**kwargs): + """Create a minimal YuanbaoAdapter mock.""" + adapter = MagicMock(spec=YuanbaoAdapter) + adapter.name = "yuanbao" + adapter._app_key = "test_key" + adapter._app_secret = "test_secret" + adapter._api_domain = "https://test.example.com" + adapter._route_env = None + adapter._bot_id = "test_bot" + adapter._ws_url = "wss://test.example.com/ws" + adapter._mark_connected = MagicMock() + adapter._mark_disconnected = MagicMock() + adapter._release_platform_lock = MagicMock() + return adapter + + +@pytest.mark.asyncio +async def test_do_reconnect_calls_set_active_on_success(): + """After a successful reconnect, set_active(adapter) must be called.""" + adapter = _make_adapter() + cm = ConnectionManager(adapter) + + # Mock the reconnect internals to succeed on first attempt + mock_ws = AsyncMock() + mock_ws.close = AsyncMock() + + with ( + patch.object(cm, "_cleanup_ws", new_callable=AsyncMock) as mock_cleanup, + patch( + "gateway.platforms.yuanbao.SignManager.force_refresh", + new_callable=AsyncMock, + return_value={"bot_id": "test_bot", "token": "test_token"}, + ), + patch("gateway.platforms.yuanbao.websockets.connect", new_callable=AsyncMock, return_value=mock_ws), + patch.object(cm, "_authenticate", new_callable=AsyncMock, return_value=True), + patch.object(cm, "_heartbeat_loop", new_callable=AsyncMock), + patch.object(cm, "_receive_loop", new_callable=AsyncMock), + patch("gateway.platforms.yuanbao.MAX_RECONNECT_ATTEMPTS", 1), + ): + # Clear any existing active instance + YuanbaoAdapter.set_active(None) + assert get_active_adapter() is None + + # Run reconnect + result = await cm._do_reconnect() + + # Reconnect should succeed + assert result is True + + # After successful reconnect, get_active() must return the adapter + assert get_active_adapter() is adapter + + +@pytest.mark.asyncio +async def test_do_reconnect_does_not_set_active_on_failure(): + """When all reconnect attempts fail, set_active should NOT be called.""" + adapter = _make_adapter() + cm = ConnectionManager(adapter) + + with ( + patch.object(cm, "_cleanup_ws", new_callable=AsyncMock), + patch( + "gateway.platforms.yuanbao.SignManager.force_refresh", + new_callable=AsyncMock, + side_effect=Exception("auth failed"), + ), + patch("gateway.platforms.yuanbao.MAX_RECONNECT_ATTEMPTS", 1), + ): + # Clear any existing active instance + YuanbaoAdapter.set_active(None) + assert get_active_adapter() is None + + # Run reconnect - should fail + result = await cm._do_reconnect() + + # Reconnect should fail + assert result is False + + # get_active() should still be None + assert get_active_adapter() is None