fix(telegram): heartbeat loop exits cleanly when bot has no get_me

CI shard test_telegram_conflict.py timed out (140s) because the new
_polling_heartbeat_loop, started by connect(), busy-spun under those
tests: they monkeypatch asyncio.sleep to instant and pass a bot double
with no get_me(), so the probe raised AttributeError (swallowed) and the
loop re-entered immediately with no real pacing, starving the event loop.

Guard the loop to return when bot.get_me is not callable — a real PTB Bot
always exposes it, so this only triggers on a torn-down app or a test
double, where there is nothing to probe. Also cancel the heartbeat task in
the conflict tests that call connect() without disconnect(), matching the
production disconnect() teardown.

Verified: test_telegram_conflict.py now runs in ~4.5s; the 22
heartbeat/reconnect tests still pass; E2E confirms a hanging get_me still
fires the reconnect ladder while a missing get_me exits without spinning.
This commit is contained in:
Teknium 2026-06-24 00:13:51 -07:00
parent 8501caf51f
commit ce802e932c
2 changed files with 35 additions and 2 deletions

View file

@ -1745,9 +1745,15 @@ class TelegramAdapter(BasePlatformAdapter):
await asyncio.sleep(HEARTBEAT_INTERVAL)
if self.has_fatal_error:
return
if not (self._app and self._app.bot):
bot = self._app.bot if self._app else None
if bot is None:
continue
await asyncio.wait_for(self._app.bot.get_me(), PROBE_TIMEOUT)
# A real PTB Bot always exposes get_me(); if it's absent the
# app isn't a live polling client (e.g. torn down or a test
# double), so there is nothing to probe — exit rather than spin.
if not callable(getattr(bot, "get_me", None)):
return
await asyncio.wait_for(bot.get_me(), PROBE_TIMEOUT)
except asyncio.CancelledError:
return
except (asyncio.TimeoutError, OSError) as probe_err: