Files
hermes-agent/tests/test_yuanbao_reconnect_set_active.py
T
Teknium 92d0bd0d73 simplify(compat): gateway — drop 30 re-exports/aliases + 2 shim modules, re-remove 3 shim-only names, repoint 24 callers + 34 test files
Per COMPAT_REMOVAL.md (internal import paths are not a stable API):

Re-exports removed
- gateway/run.py: atomic_json_write, load_dotenv, resolve_delivery_transport,
  TurnRunner, merge_pending_message_event, _arm_loop_floor_timer,
  start_loop_liveness_watchdog, DEFAULT_GATEWAY_POST_INTERRUPT_GRACE_TIMEOUT,
  _UNSET (9) — run_* mixins and tests now import from the defining module
  (gateway.delivery / gateway.run_turn_runner / gateway.platforms.base /
  gateway.shutdown_watchdog / gateway.restart / utils).
- gateway/session.py: SessionResetPolicy, normalize_whatsapp_identifier,
  TranscriptReadError, auto_continue_freshness_window (+ "_now & co." noqa
  facade) — gateway/__init__ takes SessionResetPolicy from .config; callers
  take TranscriptReadError from gateway.session_transcript.
- gateway/kanban_watchers.py: _wake_scope_id + "tests import via origin" noqa
  facade; tests import from kanban_watchers_common / _notifier.
- gateway/slash_commands.py: _model_switch_skew_guard, HISTORY_UNREADABLE.
- gateway/stream_consumer.py: escape_code_fences_for_display.
- gateway/platforms/api_server.py: "re-exported" RunIdempotencyStore comment;
  tui_gateway + tests import gateway.platforms.api_server_run_idempotency.
- gateway/platforms/__init__.py: PEP 562 __getattr__/__dir__ lazy QQAdapter /
  YuanbaoAdapter facade (no in-tree importer).
- gateway/startup_watchdog.py: whole re-export shim module deleted; the three
  in-tree callers import hermes_startup_watchdog directly.

Aliases removed
- gateway/platforms/signal.py: SignalAdapter._markdown_to_signal.
- gateway/shutdown_forensics.py: _parse_systemd_duration_to_us.
- gateway/platforms/yuanbao.py: OutboundManager.start_slow_notifier /
  cancel_slow_notifier / get_chat_lock / _chat_locks / CHAT_DICT_MAX_SIZE
  delegates; module-level get_active_adapter / send_yuanbao_direct;
  MarkdownProcessor has_unclosed_fence / ends_with_table_row /
  split_at_paragraph_boundary static pass-throughs (chunk_markdown_text stays —
  it carries yuanbao's chunking policy). tools/send_message_senders +
  tools/yuanbao_tools call YuanbaoAdapter.get_active() / sender.send_direct().

Shim-only names re-removed (earlier review-fix round 96c104c903)
- CapabilityDescriptor.from_platform_entry (+ tests/gateway/relay/test_descriptor_from_entry.py)
- SessionTurnLeaseRegistry.__len__ (+ its test)
- is_relay_media_url KEPT: download() uses it, real internal helper.

Tests that pinned a shim (startup_watchdog re-export identity, api_server
RunIdempotencyStore identity, signal wrapper parity) are dropped; tests that
pinned live behavior are repointed at the implementation.

Note: the gateway/run.py hunk of this change was swept into 00a3cfe5c9 by a
concurrent commit on the shared worktree; this commit carries the rest.

Verified in an isolated worktree (HEAD + this change only): ruff clean,
import-smoke of all 33 touched modules under a fresh HERMES_HOME,
605 passed / 1 skipped across the 39 covering test files.
2026-09-03 13:10:49 -07:00

106 lines
3.6 KiB
Python

"""test_yuanbao_reconnect_set_active.py - Verify _do_reconnect restores the active singleton.
Regression test for #58363: after a WS disconnect/reconnect cycle,
``YuanbaoAdapter.get_active()`` 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,
)
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 YuanbaoAdapter.get_active() 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 YuanbaoAdapter.get_active() 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 YuanbaoAdapter.get_active() 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 YuanbaoAdapter.get_active() is None