hermes-agent/tests/gateway/test_telegram_voice_caption_markdown.py
Teknium 2008d80a9e test(gateway): regression coverage for platform-aware voice delivery
- tests/gateway/test_base_auto_tts_output_format.py (new): the base
  adapter auto-TTS block passes an explicit .ogg output path on every
  OPUS_VOICE_PLATFORMS member (parametrized from the tts_tool set — the
  single source of truth), keeps .mp3 on non-opus platforms, honors the
  tool's success flag, and stays unique/uuid-based.
- tests/gateway/test_auto_voice_reply_format.py: runner _send_voice_reply
  parametrized across Matrix/Feishu/WhatsApp/Signal (.ogg) alongside the
  existing Telegram/Slack cases; streamed+global-auto-TTS gate regression.
- tests/gateway/test_telegram_voice_caption_markdown.py (new): caption
  MarkdownV2 formatting, entity-rejection plain fallback, overflow skip,
  and no-caption passthrough (#32029).
- test_voice_command.py filename-uuid contract test updated to point at
  build_auto_tts_output_path (construction moved there).
2026-07-28 11:55:48 -07:00

130 lines
4.6 KiB
Python

"""Telegram voice-message captions must render markdown (#32029).
``TelegramAdapter.send_voice`` used to pass captions raw with no
``parse_mode``, so auto-TTS captions carrying the agent's markdown reply
showed literal *asterisks*, backticks and [links](...). It now formats the
caption to MarkdownV2 (when the formatted text fits the 1024-char caption
cap) and falls back to the plain truncated caption when the Bot API rejects
the entities or formatting overflows.
"""
import sys
from unittest.mock import AsyncMock, MagicMock
import pytest
from gateway.config import PlatformConfig
def _ensure_telegram_mock():
if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"):
return
mod = MagicMock()
mod.error.NetworkError = type("NetworkError", (OSError,), {})
mod.error.TimedOut = type("TimedOut", (OSError,), {})
mod.error.BadRequest = type("BadRequest", (Exception,), {})
for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
sys.modules.setdefault(name, mod)
sys.modules.setdefault("telegram.error", mod.error)
_ensure_telegram_mock()
from plugins.platforms.telegram import adapter as telegram_mod # noqa: E402
from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402
def _make_adapter() -> TelegramAdapter:
adapter = TelegramAdapter(PlatformConfig(enabled=True, token="***"))
adapter._bot = MagicMock()
adapter._bot.send_voice = AsyncMock(return_value=MagicMock(message_id=1))
adapter._bot.send_audio = AsyncMock(return_value=MagicMock(message_id=2))
return adapter
def _write_ogg(tmp_path):
audio = tmp_path / "reply.ogg"
audio.write_bytes(b"\x00" * 16)
return audio
@pytest.mark.asyncio
async def test_voice_caption_gets_markdown_parse_mode(monkeypatch, tmp_path):
"""A markdown caption is MarkdownV2-formatted and sent with parse_mode."""
monkeypatch.setattr(
telegram_mod, "_probe_voice_duration_seconds", lambda _p: 3
)
adapter = _make_adapter()
result = await adapter.send_voice(
"123", str(_write_ogg(tmp_path)), caption="*bold* reply"
)
assert result.success is True
adapter._bot.send_voice.assert_awaited_once()
kwargs = adapter._bot.send_voice.await_args.kwargs
assert kwargs["parse_mode"] is not None
# format_message converts *bold* to MarkdownV2 bold; the raw caption must
# have gone through formatting rather than being passed verbatim.
assert kwargs["caption"] == adapter.format_message("*bold* reply")
@pytest.mark.asyncio
async def test_voice_caption_falls_back_to_plain_on_entity_rejection(
monkeypatch, tmp_path
):
"""A Bot API entity-parse rejection retries with the plain caption."""
monkeypatch.setattr(
telegram_mod, "_probe_voice_duration_seconds", lambda _p: 3
)
adapter = _make_adapter()
adapter._bot.send_voice = AsyncMock(
side_effect=[
Exception("Bad Request: can't parse entities"),
MagicMock(message_id=7),
]
)
result = await adapter.send_voice(
"123", str(_write_ogg(tmp_path)), caption="*bold* reply"
)
assert result.success is True
assert adapter._bot.send_voice.await_count == 2
retry_kwargs = adapter._bot.send_voice.await_args_list[1].kwargs
assert retry_kwargs["parse_mode"] is None
assert retry_kwargs["caption"] == "*bold* reply"
@pytest.mark.asyncio
async def test_voice_caption_overflow_skips_formatting(monkeypatch, tmp_path):
"""When the formatted caption exceeds 1024 UTF-16 units, send plain
(truncated) — MarkdownV2 escaping inflates length, and a truncated
formatted caption could cut an entity in half."""
monkeypatch.setattr(
telegram_mod, "_probe_voice_duration_seconds", lambda _p: 3
)
adapter = _make_adapter()
# Enough special chars that escaping pushes the formatted text over 1024.
caption = ("hello. " * 160)[:1020]
result = await adapter.send_voice("123", str(_write_ogg(tmp_path)), caption=caption)
assert result.success is True
kwargs = adapter._bot.send_voice.await_args.kwargs
assert kwargs["parse_mode"] is None
assert kwargs["caption"] == caption[:1024]
@pytest.mark.asyncio
async def test_voice_without_caption_unchanged(monkeypatch, tmp_path):
monkeypatch.setattr(
telegram_mod, "_probe_voice_duration_seconds", lambda _p: 3
)
adapter = _make_adapter()
result = await adapter.send_voice("123", str(_write_ogg(tmp_path)))
assert result.success is True
kwargs = adapter._bot.send_voice.await_args.kwargs
assert kwargs["caption"] is None
assert kwargs["parse_mode"] is None