From c711dcab134193fe8f28feb25a33bd93c42b7ad1 Mon Sep 17 00:00:00 2001 From: h Date: Fri, 28 Aug 2026 18:32:45 +0200 Subject: [PATCH] fix(telegram): final message waits for the draft push in flight --- src/beaver_gateway/frontends/telegram/drafts.py | 12 +++++++++++- tests/test_telegram.py | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/beaver_gateway/frontends/telegram/drafts.py b/src/beaver_gateway/frontends/telegram/drafts.py index 7ffdb7e..6e918fb 100644 --- a/src/beaver_gateway/frontends/telegram/drafts.py +++ b/src/beaver_gateway/frontends/telegram/drafts.py @@ -52,6 +52,7 @@ class Draft: self._broken = False self._last_sent = 0.0 self._task: asyncio.Task[None] | None = None + self._inflight: asyncio.Future[None] | None = None def start(self) -> None: if self._task is None: @@ -68,17 +69,26 @@ class Draft: self._dirty = True async def stop(self) -> None: + """Stop pushing and wait for the push in flight. + + The final ``sendMessage`` must reach Telegram after the last draft, + or the draft lands on top of the message and lingers for its 30 s. + """ if self._task is None: return self._task.cancel() with contextlib.suppress(asyncio.CancelledError): await self._task self._task = None + if self._inflight is not None: + with contextlib.suppress(Exception): + await self._inflight async def _run(self) -> None: while not self._broken: if self._dirty or time.monotonic() - self._last_sent > _KEEPALIVE: - await self._push() + self._inflight = asyncio.ensure_future(self._push()) + await asyncio.shield(self._inflight) await asyncio.sleep(self._interval) async def _push(self) -> None: diff --git a/tests/test_telegram.py b/tests/test_telegram.py index eaa49a2..7679136 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -33,6 +33,7 @@ class FakeBot: self.reactions: list[tuple[int, list[Any]]] = [] self.fail_sends = 0 self.reject_html = False + self.order: list[tuple[str, int | None]] = [] self._message_id = 100 self.session = SimpleNamespace(close=self._close) @@ -64,6 +65,7 @@ class FakeBot: message="Bad Request: can't parse entities", ) self._message_id += 1 + self.order.append(("message", message_thread_id)) self.sent.append( { "chat_id": chat_id, @@ -77,7 +79,9 @@ class FakeBot: return SimpleNamespace(message_id=self._message_id) async def send_message_draft(self, **kwargs: Any) -> bool: + await asyncio.sleep(0.08) self.drafts.append(kwargs) + self.order.append(("draft", kwargs.get("message_thread_id"))) return True async def create_forum_topic(self, chat_id, name, **_: Any) -> Any: @@ -244,6 +248,9 @@ async def test_general_is_master_and_reply_has_no_thread(stack: Stack) -> None: ) assert master is not None and master.kind == "master" assert stack.bot.drafts and stack.bot.drafts[0]["chat_id"] == USER + await asyncio.sleep(0.2) + final = max(i for i, o in enumerate(stack.bot.order) if o[0] == "message") + assert all(o[0] != "draft" for o in stack.bot.order[final:]) rows = await stack.world.conversations.queue.recent(master.id) assert [r.origin for r in rows] == ["telegram"]