add formating

This commit is contained in:
kandrusyak
2026-07-27 17:14:07 +03:00
parent 7b7ff51d0f
commit 275eca8dc5
10 changed files with 628 additions and 157 deletions

View File

@@ -5,11 +5,12 @@ from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock
from telegram.constants import ChatType
from telegram.constants import ChatType, ParseMode
from telegram.ext import ApplicationHandlerStop
from assistant_bot.auth import authentication_guard
from assistant_bot.storage import AssistantStorage
from assistant_bot.telegram_utils import render_markdown_chunks
class AuthenticationGuardTests(unittest.IsolatedAsyncioTestCase):
@@ -47,8 +48,12 @@ class AuthenticationGuardTests(unittest.IsolatedAsyncioTestCase):
await authentication_guard(update, context)
self.assertTrue(self.storage.is_user_authorized(42))
rendered = render_markdown_chunks(
"✅ **Пароль принят.** Доступ открыт — повторно вводить его не нужно."
)[0][0]
update.effective_message.reply_text.assert_awaited_once_with(
"Пароль принят. Доступ открыт — повторно вводить его не нужно."
rendered,
parse_mode=ParseMode.MARKDOWN_V2,
)
async def test_wrong_password_does_not_authorize_user(self) -> None:
@@ -58,8 +63,12 @@ class AuthenticationGuardTests(unittest.IsolatedAsyncioTestCase):
await authentication_guard(update, context)
self.assertFalse(self.storage.is_user_authorized(42))
rendered = render_markdown_chunks(
"🔐 **Неверный пароль.** Попробуй ещё раз."
)[0][0]
update.effective_message.reply_text.assert_awaited_once_with(
"Неверный пароль. Попробуй еще раз."
rendered,
parse_mode=ParseMode.MARKDOWN_V2,
)
async def test_authorized_user_passes_guard(self) -> None:

View File

@@ -0,0 +1,116 @@
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()

View File

@@ -4,7 +4,10 @@ from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from telegram.constants import ParseMode
from assistant_bot.handlers import private_voice
from assistant_bot.telegram_utils import render_markdown_chunks
class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
@@ -42,8 +45,12 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
await private_voice(update, context)
rendered = render_markdown_chunks(
"🎙️ **Голосовое сообщение слишком длинное.** Максимум: `120` сек."
)[0][0]
update.message.reply_text.assert_awaited_once_with(
"Голосовое сообщение слишком длинное. Максимум: 120 сек."
rendered,
parse_mode=ParseMode.MARKDOWN_V2,
)
recognizer.transcribe.assert_not_called()
@@ -64,7 +71,13 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
telegram_file.download_to_drive.assert_awaited_once_with(
custom_path=temporary_path
)
status.edit_text.assert_awaited_once_with("Распознано: Напомни позвонить")
rendered = render_markdown_chunks(
"🎙️ **Распознано:** Напомни позвонить"
)[0][0]
status.edit_text.assert_awaited_once_with(
rendered,
parse_mode=ParseMode.MARKDOWN_V2,
)
run_agent.assert_awaited_once_with(
update,
context,