diff --git a/gateway/relay/adapter.py b/gateway/relay/adapter.py index 7aa65468ce7..0cf5fb18a60 100644 --- a/gateway/relay/adapter.py +++ b/gateway/relay/adapter.py @@ -106,7 +106,24 @@ class RelayAdapter(BasePlatformAdapter): return self.descriptor.supports_draft_streaming # ── abstract methods (delegated to the transport) ──────────────────── - async def connect(self) -> bool: + async def connect(self, *, is_reconnect: bool = False) -> bool: + # ``is_reconnect`` is part of the BasePlatformAdapter.connect contract: + # the gateway's reconnect watcher (gateway/run.py) re-establishes a + # platform after a fatal adapter error by building a fresh adapter and + # calling ``connect(is_reconnect=True)``. Relay MUST accept the kwarg or + # that recovery path raises TypeError and the relay platform can never + # come back through the watcher. + # + # Relay deliberately IGNORES the flag. The flag exists so adapters with a + # server-side update queue (e.g. Telegram's Bot API) preserve that queue + # across an outage instead of dropping it (#46621). Relay has no such + # gateway-side queue: messages buffered during a gap live in the + # CONNECTOR's durable buffer and are replayed when the transport + # re-handshakes. Routine WS drops are handled entirely by the transport's + # own reconnect supervisor (WebSocketRelayTransport, reconnect=True); + # a watcher-driven reconnect builds a fresh transport from scratch (the + # fatal-error handler disconnect()s the old adapter first, cancelling its + # supervisor), so there is nothing at the adapter layer to preserve. if self._transport is None: raise RuntimeError("RelayAdapter has no transport configured") self._transport.set_inbound_handler(self._on_inbound) diff --git a/tests/gateway/relay/test_relay_adapter.py b/tests/gateway/relay/test_relay_adapter.py index 319ccf337ce..b7cea4b3946 100644 --- a/tests/gateway/relay/test_relay_adapter.py +++ b/tests/gateway/relay/test_relay_adapter.py @@ -69,6 +69,40 @@ async def test_connect_without_transport_raises(): await a.connect() +@pytest.mark.asyncio +async def test_connect_accepts_is_reconnect_kwarg(): + """Regression: RelayAdapter.connect must accept the BasePlatformAdapter + contract's ``is_reconnect`` kwarg. The gateway reconnect watcher recovers a + platform after a fatal adapter error by calling ``connect(is_reconnect=True)`` + (gateway/run.py); before the fix, RelayAdapter.connect was bare ``connect()`` + and that recovery path raised ``TypeError: connect() got an unexpected + keyword argument 'is_reconnect'`` (observed live: relay never reconnected, + no DMs). It must reach the SAME transport-less RuntimeError as connect() — + i.e. accept the kwarg, never TypeError on it.""" + a = _adapter() + with pytest.raises(RuntimeError, match="no transport"): + await a.connect(is_reconnect=True) + + +def test_connect_signature_matches_base_contract(): + """The is_reconnect parameter must be keyword-accepting and default False, + matching BasePlatformAdapter.connect, so the reconnect watcher's + ``connect(is_reconnect=...)`` call is valid for relay as for every other + adapter.""" + import inspect + + from gateway.platforms.base import BasePlatformAdapter + + sig = inspect.signature(RelayAdapter.connect) + base_sig = inspect.signature(BasePlatformAdapter.connect) + assert "is_reconnect" in sig.parameters + param = sig.parameters["is_reconnect"] + base_param = base_sig.parameters["is_reconnect"] + # Keyword-acceptable (KEYWORD_ONLY here, matching the base) with a False default. + assert param.kind is base_param.kind + assert param.default is False + + @pytest.mark.asyncio async def test_send_without_transport_returns_failure(): a = _adapter()