Files
AstrBot/tests/test_telegram_adapter.py
Shujakuin 9c14a50b06 fix: split long telegram final segments (#7432)
* fix: split long telegram final segments

* test: refine telegram adapter helpers

* Update astrbot/core/platform/sources/telegram/tg_event.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Update tg_event.py

* chore: ruff format

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>
2026-04-10 14:36:47 +08:00

215 lines
7.3 KiB
Python

import asyncio
import importlib
import sys
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import astrbot.api.message_components as Comp
from tests.fixtures.helpers import (
create_mock_file,
create_mock_update,
make_platform_config,
)
from tests.fixtures.mocks.telegram import create_mock_telegram_modules
_TELEGRAM_PLATFORM_ADAPTER = None
_TELEGRAM_PLATFORM_EVENT = None
_TELEGRAM_MODULES: dict[str, object] = {}
def _build_telegram_patched_modules():
mocks = create_mock_telegram_modules()
return {
"telegram": mocks["telegram"],
"telegram.constants": mocks["telegram"].constants,
"telegram.error": mocks["telegram"].error,
"telegram.ext": mocks["telegram.ext"],
"telegramify_markdown": mocks["telegramify_markdown"],
"apscheduler": mocks["apscheduler"],
"apscheduler.schedulers": mocks["apscheduler"].schedulers,
"apscheduler.schedulers.asyncio": mocks["apscheduler"].schedulers.asyncio,
"apscheduler.schedulers.background": mocks["apscheduler"].schedulers.background,
}
def _load_telegram_module(module_name: str):
module = _TELEGRAM_MODULES.get(module_name)
if module is not None:
return module
with patch.dict(sys.modules, _build_telegram_patched_modules()):
sys.modules.pop(module_name, None)
module = importlib.import_module(module_name)
sys.modules[module_name] = module
_TELEGRAM_MODULES[module_name] = module
return module
def _load_telegram_adapter():
global _TELEGRAM_PLATFORM_ADAPTER
if _TELEGRAM_PLATFORM_ADAPTER is not None:
return _TELEGRAM_PLATFORM_ADAPTER
module = _load_telegram_module("astrbot.core.platform.sources.telegram.tg_adapter")
_TELEGRAM_PLATFORM_ADAPTER = module.TelegramPlatformAdapter
return _TELEGRAM_PLATFORM_ADAPTER
def _load_telegram_platform_event():
global _TELEGRAM_PLATFORM_EVENT
if _TELEGRAM_PLATFORM_EVENT is not None:
return _TELEGRAM_PLATFORM_EVENT
module = _load_telegram_module("astrbot.core.platform.sources.telegram.tg_event")
_TELEGRAM_PLATFORM_EVENT = module.TelegramPlatformEvent
return _TELEGRAM_PLATFORM_EVENT
def _build_context() -> MagicMock:
context = MagicMock()
context.bot.username = "test_bot"
context.bot.id = 12345678
return context
@pytest.mark.asyncio
async def test_telegram_document_caption_populates_message_text_and_plain():
TelegramPlatformAdapter = _load_telegram_adapter()
adapter = TelegramPlatformAdapter(
make_platform_config("telegram"),
{},
asyncio.Queue(),
)
document = create_mock_file("https://api.telegram.org/file/test/report.md")
document.file_name = "report.md"
mention = MagicMock(type="mention", offset=0, length=6)
update = create_mock_update(
message_text=None,
document=document,
caption="@alice 请总结这份文档",
caption_entities=[mention],
)
result = await adapter.convert_message(update, _build_context())
assert result is not None
assert result.message_str == "@alice 请总结这份文档"
assert any(isinstance(component, Comp.File) for component in result.message)
assert any(
isinstance(component, Comp.Plain) and component.text == "@alice 请总结这份文档"
for component in result.message
)
assert any(
isinstance(component, Comp.At) and component.qq == "alice"
for component in result.message
)
@pytest.mark.asyncio
async def test_telegram_video_caption_populates_message_text_and_plain():
TelegramPlatformAdapter = _load_telegram_adapter()
adapter = TelegramPlatformAdapter(
make_platform_config("telegram"),
{},
asyncio.Queue(),
)
video = create_mock_file("https://api.telegram.org/file/test/lesson.mp4")
video.file_name = "lesson.mp4"
update = create_mock_update(
message_text=None,
video=video,
caption="这段视频讲了什么",
)
result = await adapter.convert_message(update, _build_context())
assert result is not None
assert result.message_str == "这段视频讲了什么"
assert any(isinstance(component, Comp.Video) for component in result.message)
assert any(
isinstance(component, Comp.Plain) and component.text == "这段视频讲了什么"
for component in result.message
)
@pytest.mark.asyncio
async def test_telegram_voice_message_creates_record_component(tmp_path):
TelegramPlatformAdapter = _load_telegram_adapter()
adapter = TelegramPlatformAdapter(
make_platform_config("telegram"),
{},
asyncio.Queue(),
)
voice = create_mock_file("https://api.telegram.org/file/test/voice.oga")
update = create_mock_update(
message_text=None,
voice=voice,
)
wav_path = tmp_path / "voice.oga.wav"
convert_message_globals = adapter.convert_message.__func__.__globals__
with patch.dict(
convert_message_globals,
{
"get_astrbot_temp_path": MagicMock(return_value=str(tmp_path)),
"download_file": AsyncMock(),
"convert_audio_to_wav": AsyncMock(return_value=str(wav_path)),
},
):
result = await adapter.convert_message(update, _build_context())
assert result is not None
assert len(result.message) == 1
assert isinstance(result.message[0], Comp.Record)
assert result.message[0].file == str(wav_path)
assert result.message[0].path == str(wav_path)
assert result.message[0].url == str(wav_path)
@pytest.mark.asyncio
async def test_telegram_final_segment_splits_long_markdown_messages():
TelegramPlatformEvent = _load_telegram_platform_event()
client = MagicMock()
client.send_message = AsyncMock()
event = TelegramPlatformEvent("msg", MagicMock(), MagicMock(), "session", client)
delta = "A" * (TelegramPlatformEvent.MAX_MESSAGE_LENGTH + 32)
payload = {"chat_id": "123456"}
await event._send_final_segment(delta, payload)
assert client.send_message.await_count == 2
first_call = client.send_message.await_args_list[0].kwargs
second_call = client.send_message.await_args_list[1].kwargs
assert len(first_call["text"]) == TelegramPlatformEvent.MAX_MESSAGE_LENGTH
assert len(second_call["text"]) == 32
assert first_call["parse_mode"] == "MarkdownV2"
assert second_call["parse_mode"] == "MarkdownV2"
@pytest.mark.asyncio
async def test_telegram_final_segment_splits_long_plaintext_when_markdown_fails():
TelegramPlatformEvent = _load_telegram_platform_event()
client = MagicMock()
client.send_message = AsyncMock()
event = TelegramPlatformEvent("msg", MagicMock(), MagicMock(), "session", client)
delta = "B" * (TelegramPlatformEvent.MAX_MESSAGE_LENGTH + 18)
payload = {"chat_id": "123456"}
with patch(
"astrbot.core.platform.sources.telegram.tg_event.telegramify_markdown.markdownify",
side_effect=Exception("boom"),
):
await event._send_final_segment(delta, payload)
assert client.send_message.await_count == 2
first_call = client.send_message.await_args_list[0].kwargs
second_call = client.send_message.await_args_list[1].kwargs
assert len(first_call["text"]) == TelegramPlatformEvent.MAX_MESSAGE_LENGTH
assert len(second_call["text"]) == 18
assert "parse_mode" not in first_call
assert "parse_mode" not in second_call