import unittest from types import SimpleNamespace from unittest.mock import AsyncMock from telegram.constants import ParseMode from telegram.error import BadRequest from assistant_bot.telegram_utils import ( FORMATTING_FALLBACK_NOTICE, build_inline_results, escape_markdown_text, render_markdown_chunks, reply_markdown, ) class MarkdownRenderingTests(unittest.TestCase): def test_converts_common_markdown_to_markdown_v2(self) -> None: chunks = render_markdown_chunks( "**📝 Заметки**\n\n- `#3` Проект\\_v2\\.0" ) self.assertEqual(len(chunks), 1) markdown_v2, plain_text = chunks[0] self.assertIn("*📝 Заметки*", markdown_v2) self.assertIn("Проект\\_v2\\.0", markdown_v2) self.assertNotIn("**", markdown_v2) self.assertIn("Проект_v2.0", plain_text) def test_splits_long_formatted_text_without_losing_format(self) -> None: chunks = render_markdown_chunks(f"**{'слово ' * 999}слово**") self.assertGreater(len(chunks), 1) for markdown_v2, plain_text in chunks: self.assertLessEqual(len(plain_text.encode("utf-16-le")) // 2, 3900) self.assertLessEqual(len(markdown_v2.encode("utf-16-le")) // 2, 3900) self.assertTrue(markdown_v2.startswith("*")) self.assertTrue(markdown_v2.rstrip().endswith("*")) def test_splits_by_rendered_markdown_v2_length(self) -> None: chunks = render_markdown_chunks(escape_markdown_text("_" * 5000)) self.assertGreater(len(chunks), 2) for markdown_v2, _plain_text in chunks: self.assertLessEqual(len(markdown_v2.encode("utf-16-le")) // 2, 3900) def test_inline_results_use_markdown_v2(self) -> None: results = build_inline_results("проект_v2") bold_content = results[1].input_message_content self.assertEqual(bold_content.parse_mode, ParseMode.MARKDOWN_V2) self.assertEqual(bold_content.message_text, "*проект\\_v2*") class MarkdownDeliveryTests(unittest.IsolatedAsyncioTestCase): async def test_retries_plain_text_with_notice_when_telegram_rejects_markup( self, ) -> None: fallback_message = SimpleNamespace() message = SimpleNamespace( reply_text=AsyncMock( side_effect=[ BadRequest("Can't parse entities"), fallback_message, ] ) ) sent = await reply_markdown(message, "**Готово.**") self.assertIs(sent, fallback_message) self.assertEqual(message.reply_text.await_count, 2) formatted_call, fallback_call = message.reply_text.await_args_list self.assertEqual( formatted_call.kwargs["parse_mode"], ParseMode.MARKDOWN_V2, ) self.assertNotIn("parse_mode", fallback_call.kwargs) self.assertEqual( fallback_call.args[0], f"Готово.\n\n{FORMATTING_FALLBACK_NOTICE}", ) async def test_places_fallback_notice_only_in_last_chunk(self) -> None: first_message = SimpleNamespace() markdown = f"**{'слово ' * 999}слово**" expected_chunks = render_markdown_chunks(markdown) call_number = 0 async def send(*_args, **_kwargs): nonlocal call_number call_number += 1 if call_number == 1: raise BadRequest("Can't parse entities") return first_message if call_number == 2 else SimpleNamespace() message = SimpleNamespace(reply_text=AsyncMock(side_effect=send)) await reply_markdown(message, markdown) self.assertEqual(message.reply_text.await_count, len(expected_chunks) + 1) first_plain_call = message.reply_text.await_args_list[1] last_formatted_call = message.reply_text.await_args_list[-1] self.assertNotIn(FORMATTING_FALLBACK_NOTICE, first_plain_call.args[0]) self.assertIn( "Не удалось применить форматирование", last_formatted_call.args[0], ) self.assertEqual( last_formatted_call.kwargs["parse_mode"], ParseMode.MARKDOWN_V2, ) if __name__ == "__main__": unittest.main()