add formating
This commit is contained in:
@@ -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:
|
||||
|
||||
116
tests/test_telegram_utils.py
Normal file
116
tests/test_telegram_utils.py
Normal 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()
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user