fix(telegram): wait for application readiness

This commit is contained in:
Xubin Ren
2026-08-18 00:41:57 +08:00
committed by chengyongru
parent 302015fde5
commit 8a928592c5
2 changed files with 85 additions and 54 deletions
+2 -2
View File
@@ -736,13 +736,13 @@ class TelegramChannel(BaseChannel):
the message as delivered, so raise once the wait runs out. None means the the message as delivered, so raise once the wait runs out. None means the
channel is stopped: nothing left to deliver. channel is stopped: nothing left to deliver.
""" """
if self._app is not None: if self._app_ready.is_set() and self._app is not None:
return self._app return self._app
if not self._running: if not self._running:
return None return None
with suppress(asyncio.TimeoutError): with suppress(asyncio.TimeoutError):
await asyncio.wait_for(self._app_ready.wait(), APP_RESTART_SEND_WAIT_SECONDS) await asyncio.wait_for(self._app_ready.wait(), APP_RESTART_SEND_WAIT_SECONDS)
if self._app is None: if not self._app_ready.is_set() or self._app is None:
raise RuntimeError("Telegram application is restarting; message not delivered") raise RuntimeError("Telegram application is restarting; message not delivered")
return self._app return self._app
@@ -157,6 +157,14 @@ class _FakeBuilder:
return self.app return self.app
def _install_ready_app(channel: TelegramChannel) -> _FakeApp:
"""Install the ready app state expected by ordinary send tests."""
app = _FakeApp(lambda: None)
channel._app = app
channel._app_ready.set()
return app
def _make_telegram_update( def _make_telegram_update(
*, *,
chat_type: str = "group", chat_type: str = "group",
@@ -601,6 +609,29 @@ async def test_send_waits_for_rebuild_to_finish(monkeypatch) -> None:
assert [m["text"] for m in app.bot.sent_messages] == ["hello"] assert [m["text"] for m in app.bot.sent_messages] == ["hello"]
@pytest.mark.asyncio
async def test_send_waits_for_partially_initialized_app(monkeypatch) -> None:
"""A built app is not available for sends until startup marks it ready."""
monkeypatch.setattr("nanobot.channels.telegram.runtime.APP_RESTART_SEND_WAIT_SECONDS", 5.0)
config = TelegramConfig(enabled=True, token="123:abc", allow_from=["*"])
channel = TelegramChannel(config, MessageBus())
app = _FakeApp(lambda: None)
channel._running = True
channel._app = app
send_task = asyncio.create_task(
channel.send(OutboundMessage(channel="telegram", chat_id="123", content="hello"))
)
await asyncio.sleep(0)
assert not send_task.done()
channel._app_ready.set()
await send_task
assert [m["text"] for m in app.bot.sent_messages] == ["hello"]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_liveness_tracked_request_stamps_on_round_trip() -> None: async def test_liveness_tracked_request_stamps_on_round_trip() -> None:
from nanobot.channels.telegram.runtime import _LivenessTrackedRequest from nanobot.channels.telegram.runtime import _LivenessTrackedRequest
@@ -725,7 +756,7 @@ async def test_send_text_retries_on_timeout() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
call_count = 0 call_count = 0
original_send = channel._app.bot.send_message original_send = channel._app.bot.send_message
@@ -760,7 +791,7 @@ async def test_send_text_gives_up_after_max_retries() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
async def always_timeout(**kwargs): async def always_timeout(**kwargs):
raise TimedOut() raise TimedOut()
@@ -787,7 +818,7 @@ async def test_send_rich_capability_error_latches_and_falls_back() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], rich_messages=True), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], rich_messages=True),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.do_api_request = AsyncMock(side_effect=BadRequest("Method not found")) channel._app.bot.do_api_request = AsyncMock(side_effect=BadRequest("Method not found"))
await channel.send(OutboundMessage(channel="telegram", chat_id="123", content="**hello**")) await channel.send(OutboundMessage(channel="telegram", chat_id="123", content="**hello**"))
@@ -806,7 +837,7 @@ async def test_send_rich_bad_request_does_not_latch_capability() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], rich_messages=True), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], rich_messages=True),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.do_api_request = AsyncMock( channel._app.bot.do_api_request = AsyncMock(
side_effect=BadRequest("Bad Request: message to reply not found") side_effect=BadRequest("Bad Request: message to reply not found")
) )
@@ -825,7 +856,7 @@ async def test_rich_messages_default_skips_send_rich_message() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.do_api_request = AsyncMock() channel._app.bot.do_api_request = AsyncMock()
await channel.send(OutboundMessage(channel="telegram", chat_id="123", content="**hello**")) await channel.send(OutboundMessage(channel="telegram", chat_id="123", content="**hello**"))
@@ -912,7 +943,7 @@ async def test_send_delta_stream_end_raises_and_keeps_buffer_on_failure() -> Non
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock(side_effect=RuntimeError("boom")) channel._app.bot.edit_message_text = AsyncMock(side_effect=RuntimeError("boom"))
channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0) channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0)
@@ -928,7 +959,7 @@ async def test_send_delta_merge_next_preserves_buffer() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._stream_bufs["123"] = _StreamBuf( channel._stream_bufs["123"] = _StreamBuf(
text="first-", text="first-",
@@ -957,7 +988,7 @@ async def test_send_delta_stream_end_treats_not_modified_as_success() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("Message is not modified")) channel._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("Message is not modified"))
channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0, stream_id="s:0") channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0, stream_id="s:0")
@@ -977,7 +1008,7 @@ async def test_send_delta_stream_end_does_not_fallback_on_network_timeout(
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
monkeypatch.setattr("nanobot.channels.telegram.runtime._SEND_RETRY_BASE_DELAY", 0) monkeypatch.setattr("nanobot.channels.telegram.runtime._SEND_RETRY_BASE_DELAY", 0)
# _call_with_retry retries TimedOut up to 3 times, so the mock will be called # _call_with_retry retries TimedOut up to 3 times, so the mock will be called
# multiple times but all calls must be with parse_mode="HTML" (no plain fallback). # multiple times but all calls must be with parse_mode="HTML" (no plain fallback).
@@ -1005,7 +1036,7 @@ async def test_send_delta_stream_end_does_not_fallback_on_network_error() -> Non
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock(side_effect=NetworkError("connection reset")) channel._app.bot.edit_message_text = AsyncMock(side_effect=NetworkError("connection reset"))
channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0) channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0)
@@ -1029,7 +1060,7 @@ async def test_send_delta_stream_end_falls_back_on_bad_request() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
# First call (HTML) raises BadRequest, second call (plain) succeeds # First call (HTML) raises BadRequest, second call (plain) succeeds
channel._app.bot.edit_message_text = AsyncMock( channel._app.bot.edit_message_text = AsyncMock(
@@ -1061,7 +1092,7 @@ async def test_send_delta_stream_end_splits_oversized_reply() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
@@ -1095,7 +1126,7 @@ async def test_send_delta_stream_end_html_expansion_does_not_overflow() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
@@ -1126,7 +1157,7 @@ async def test_send_delta_stream_end_splits_long_code_block_before_html_renderin
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
@@ -1155,7 +1186,7 @@ async def test_send_delta_new_stream_id_replaces_stale_buffer() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._stream_bufs["123"] = _StreamBuf( channel._stream_bufs["123"] = _StreamBuf(
text="hello", text="hello",
message_id=7, message_id=7,
@@ -1179,7 +1210,7 @@ async def test_send_delta_incremental_edit_treats_not_modified_as_success() -> N
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0, stream_id="s:0") channel._stream_bufs["123"] = _StreamBuf(text="hello", message_id=7, last_edit=0.0, stream_id="s:0")
channel._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("Message is not modified")) channel._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("Message is not modified"))
@@ -1197,7 +1228,7 @@ async def test_send_delta_incremental_edit_splits_oversized_buffer() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
@@ -1234,7 +1265,7 @@ async def test_send_delta_incremental_html_expansion_does_not_overflow() -> None
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=99))
@@ -1268,7 +1299,7 @@ async def test_send_delta_incremental_html_parse_failure_falls_back_to_plain() -
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.edit_message_text = AsyncMock( channel._app.bot.edit_message_text = AsyncMock(
side_effect=[BadRequest("Can't parse entities"), None] side_effect=[BadRequest("Can't parse entities"), None]
) )
@@ -1302,7 +1333,7 @@ async def test_send_delta_initial_send_keeps_message_in_thread() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
await channel.send_delta( await channel.send_delta(
"123", "123",
@@ -1375,7 +1406,7 @@ def test_is_allowed_rejects_invalid_legacy_telegram_sender_shapes() -> None:
async def test_send_progress_keeps_message_in_topic() -> None: async def test_send_progress_keeps_message_in_topic() -> None:
config = TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]) config = TelegramConfig(enabled=True, token="123:abc", allow_from=["*"])
channel = TelegramChannel(config, MessageBus()) channel = TelegramChannel(config, MessageBus())
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
await channel.send( await channel.send(
OutboundMessage( OutboundMessage(
@@ -1394,7 +1425,7 @@ async def test_send_progress_keeps_message_in_topic() -> None:
async def test_send_reply_infers_topic_from_message_id_cache() -> None: async def test_send_reply_infers_topic_from_message_id_cache() -> None:
config = TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], reply_to_message=True) config = TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], reply_to_message=True)
channel = TelegramChannel(config, MessageBus()) channel = TelegramChannel(config, MessageBus())
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._message_threads[("123", 10)] = 42 channel._message_threads[("123", 10)] = 42
await channel.send( await channel.send(
@@ -1416,7 +1447,7 @@ async def test_send_remote_media_url_after_security_validation(monkeypatch) -> N
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
monkeypatch.setattr("nanobot.channels.telegram.runtime.validate_url_target", lambda url: (True, "")) monkeypatch.setattr("nanobot.channels.telegram.runtime.validate_url_target", lambda url: (True, ""))
await channel.send( await channel.send(
@@ -1444,7 +1475,7 @@ async def test_send_local_media_preserves_filename(tmp_path: Path) -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
attachment = tmp_path / "report.final.md" attachment = tmp_path / "report.final.md"
attachment.write_bytes(b"# Report\n") attachment.write_bytes(b"# Report\n")
@@ -1474,7 +1505,7 @@ async def test_send_blocks_unsafe_remote_media_url(monkeypatch) -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
monkeypatch.setattr( monkeypatch.setattr(
"nanobot.channels.telegram.runtime.validate_url_target", "nanobot.channels.telegram.runtime.validate_url_target",
lambda url: (False, "Blocked: example.com resolves to private/internal address 127.0.0.1"), lambda url: (False, "Blocked: example.com resolves to private/internal address 127.0.0.1"),
@@ -1505,7 +1536,7 @@ async def test_group_policy_mention_ignores_unmentioned_group_message() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
@@ -1527,7 +1558,7 @@ async def test_group_policy_mention_accepts_text_mention_and_caches_bot_identity
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
@@ -1551,7 +1582,7 @@ async def test_group_policy_mention_accepts_caption_mention() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
@@ -1577,7 +1608,7 @@ async def test_group_policy_mention_accepts_reply_to_bot() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="mention"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
@@ -1599,7 +1630,7 @@ async def test_group_policy_open_accepts_plain_group_message() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
@@ -1627,7 +1658,7 @@ async def test_extract_reply_context_no_reply() -> None:
async def test_extract_reply_context_with_text() -> None: async def test_extract_reply_context_with_text() -> None:
"""When reply has text, return prefixed string.""" """When reply has text, return prefixed string."""
channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus()) channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus())
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
reply = SimpleNamespace(text="Hello world", caption=None, from_user=SimpleNamespace(id=2, username="testuser", first_name="Test")) reply = SimpleNamespace(text="Hello world", caption=None, from_user=SimpleNamespace(id=2, username="testuser", first_name="Test"))
message = SimpleNamespace(reply_to_message=reply) message = SimpleNamespace(reply_to_message=reply)
assert await channel._extract_reply_context(message) == "[Reply to @testuser: Hello world]" assert await channel._extract_reply_context(message) == "[Reply to @testuser: Hello world]"
@@ -1637,7 +1668,7 @@ async def test_extract_reply_context_with_text() -> None:
async def test_extract_reply_context_with_caption_only() -> None: async def test_extract_reply_context_with_caption_only() -> None:
"""When reply has only caption (no text), caption is used.""" """When reply has only caption (no text), caption is used."""
channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus()) channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus())
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
reply = SimpleNamespace(text=None, caption="Photo caption", from_user=SimpleNamespace(id=2, username=None, first_name="Test")) reply = SimpleNamespace(text=None, caption="Photo caption", from_user=SimpleNamespace(id=2, username=None, first_name="Test"))
message = SimpleNamespace(reply_to_message=reply) message = SimpleNamespace(reply_to_message=reply)
assert await channel._extract_reply_context(message) == "[Reply to Test: Photo caption]" assert await channel._extract_reply_context(message) == "[Reply to Test: Photo caption]"
@@ -1647,7 +1678,7 @@ async def test_extract_reply_context_with_caption_only() -> None:
async def test_extract_reply_context_truncation() -> None: async def test_extract_reply_context_truncation() -> None:
"""Reply text is truncated at TELEGRAM_REPLY_CONTEXT_MAX_LEN.""" """Reply text is truncated at TELEGRAM_REPLY_CONTEXT_MAX_LEN."""
channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus()) channel = TelegramChannel(TelegramConfig(enabled=True, token="123:abc"), MessageBus())
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
long_text = "x" * (TELEGRAM_REPLY_CONTEXT_MAX_LEN + 100) long_text = "x" * (TELEGRAM_REPLY_CONTEXT_MAX_LEN + 100)
reply = SimpleNamespace(text=long_text, caption=None, from_user=SimpleNamespace(id=2, username=None, first_name=None)) reply = SimpleNamespace(text=long_text, caption=None, from_user=SimpleNamespace(id=2, username=None, first_name=None))
message = SimpleNamespace(reply_to_message=reply) message = SimpleNamespace(reply_to_message=reply)
@@ -1674,7 +1705,7 @@ async def test_on_message_includes_reply_context() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
handled.append(kwargs) handled.append(kwargs)
@@ -1706,7 +1737,7 @@ async def test_download_message_media_returns_path_when_download_succeeds(
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.get_file = AsyncMock( channel._app.bot.get_file = AsyncMock(
return_value=SimpleNamespace(download_to_drive=AsyncMock(return_value=None)) return_value=SimpleNamespace(download_to_drive=AsyncMock(return_value=None))
) )
@@ -1833,7 +1864,7 @@ async def test_on_message_reply_to_media_fallback_when_download_fails() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.get_file = None channel._app.bot.get_file = None
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
@@ -1916,7 +1947,7 @@ async def test_forward_command_does_not_inject_reply_context() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
handled.append(kwargs) handled.append(kwargs)
@@ -1936,7 +1967,7 @@ async def test_forward_command_pairs_unauthorized_private_user(monkeypatch) -> N
TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
monkeypatch.setattr( monkeypatch.setattr(
"nanobot.channels.base.generate_code", lambda _ch, _sid: "ABCD-EFGH" "nanobot.channels.base.generate_code", lambda _ch, _sid: "ABCD-EFGH"
) )
@@ -1953,7 +1984,7 @@ async def test_forward_command_preserves_dream_log_args_and_strips_bot_suffix()
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
@@ -1974,7 +2005,7 @@ async def test_forward_command_normalizes_telegram_safe_dream_aliases() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
@@ -2049,7 +2080,7 @@ async def test_on_start_sends_pairing_code_to_unauthorized_private_user(monkeypa
TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
update = _make_telegram_update(text="/start", chat_type="private") update = _make_telegram_update(text="/start", chat_type="private")
update.message.reply_text = AsyncMock() update.message.reply_text = AsyncMock()
monkeypatch.setattr( monkeypatch.setattr(
@@ -2069,7 +2100,7 @@ async def test_on_help_sends_pairing_code_to_unauthorized_private_user(monkeypat
TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
update = _make_telegram_update(text="/help", chat_type="private") update = _make_telegram_update(text="/help", chat_type="private")
update.message.reply_text = AsyncMock() update.message.reply_text = AsyncMock()
monkeypatch.setattr( monkeypatch.setattr(
@@ -2091,7 +2122,7 @@ async def test_on_message_pairs_unauthorized_private_user_before_side_effects(
TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["999"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
started_typing: list[str] = [] started_typing: list[str] = []
channel._start_typing = lambda chat_id: started_typing.append(chat_id) channel._start_typing = lambda chat_id: started_typing.append(chat_id)
channel._add_reaction = AsyncMock(return_value=None) channel._add_reaction = AsyncMock(return_value=None)
@@ -2116,7 +2147,7 @@ async def test_on_message_location_content() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
handled.append(kwargs) handled.append(kwargs)
@@ -2138,7 +2169,7 @@ async def test_on_message_location_with_text() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], group_policy="open"),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
handled = [] handled = []
async def capture_handle(**kwargs) -> None: async def capture_handle(**kwargs) -> None:
handled.append(kwargs) handled.append(kwargs)
@@ -2202,7 +2233,7 @@ async def test_send_text_does_not_fallback_on_network_timeout() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
call_count = 0 call_count = 0
@@ -2239,7 +2270,7 @@ async def test_send_text_does_not_fallback_on_network_error() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
call_count = 0 call_count = 0
@@ -2276,7 +2307,7 @@ async def test_send_text_falls_back_on_bad_request() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
original_send = channel._app.bot.send_message original_send = channel._app.bot.send_message
html_call_count = 0 html_call_count = 0
@@ -2314,7 +2345,7 @@ async def test_send_text_bad_request_plain_fallback_exhausted() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
call_count = 0 call_count = 0
@@ -2437,7 +2468,7 @@ async def test_send_delta_mid_stream_strips_markdown() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"]),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42)) channel._app.bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42))
channel._app.bot.edit_message_text = AsyncMock() channel._app.bot.edit_message_text = AsyncMock()
@@ -2538,7 +2569,7 @@ async def test_send_falls_back_buttons_to_inline_text_when_flag_off() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], inline_keyboards=False), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], inline_keyboards=False),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
await channel.send( await channel.send(
OutboundMessage( OutboundMessage(
@@ -2566,7 +2597,7 @@ async def test_send_uses_native_keyboard_when_flag_on() -> None:
TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], inline_keyboards=True), TelegramConfig(enabled=True, token="123:abc", allow_from=["*"], inline_keyboards=True),
MessageBus(), MessageBus(),
) )
channel._app = _FakeApp(lambda: None) _install_ready_app(channel)
await channel.send( await channel.send(
OutboundMessage( OutboundMessage(