/ tests / gateway / test_safe_adapter_disconnect.py
test_safe_adapter_disconnect.py
 1  """Regression tests: failed-connect path must call adapter.disconnect().
 2  
 3  When adapter.connect() returns False or raises, the adapter may have
 4  allocated resources (aiohttp.ClientSession, poll tasks, child
 5  subprocesses) before giving up. Without a defensive disconnect() call
 6  these leak and surface as "Unclosed client session" warnings at
 7  process exit (seen on the 2026-04-18 18:08:16 gateway restart).
 8  
 9  The fix: gateway/run.py wraps each adapter connect() with a safety-net
10  call to _safe_adapter_disconnect() in the failure branches.
11  """
12  
13  from unittest.mock import AsyncMock, MagicMock
14  
15  import pytest
16  
17  from gateway.config import Platform
18  from gateway.run import GatewayRunner
19  
20  
21  @pytest.fixture
22  def bare_runner():
23      """A GatewayRunner shell that only needs to support _safe_adapter_disconnect."""
24      return object.__new__(GatewayRunner)
25  
26  
27  @pytest.mark.asyncio
28  async def test_safe_disconnect_calls_adapter_disconnect(bare_runner):
29      """The helper forwards to adapter.disconnect()."""
30      adapter = MagicMock()
31      adapter.disconnect = AsyncMock(return_value=None)
32  
33      await bare_runner._safe_adapter_disconnect(adapter, Platform.TELEGRAM)
34  
35      adapter.disconnect.assert_awaited_once()
36  
37  
38  @pytest.mark.asyncio
39  async def test_safe_disconnect_swallows_exceptions(bare_runner):
40      """An exception in adapter.disconnect() must not propagate — the
41      caller is already on an error path."""
42      adapter = MagicMock()
43      adapter.disconnect = AsyncMock(side_effect=RuntimeError("partial init"))
44  
45      # Must NOT raise
46      await bare_runner._safe_adapter_disconnect(adapter, Platform.TELEGRAM)
47  
48      adapter.disconnect.assert_awaited_once()
49  
50  
51  @pytest.mark.asyncio
52  async def test_safe_disconnect_handles_none_platform(bare_runner):
53      """Logging path must tolerate platform=None."""
54      adapter = MagicMock()
55      adapter.disconnect = AsyncMock(side_effect=ValueError("nope"))
56  
57      await bare_runner._safe_adapter_disconnect(adapter, None)
58  
59      adapter.disconnect.assert_awaited_once()