import asyncio import unittest from unittest.mock import AsyncMock, patch import bot class DeployNoticeTests(unittest.IsolatedAsyncioTestCase): async def test_sends_once_per_new_version_only_in_development(self): fake_bot = AsyncMock() with ( patch.object(bot, "DEPLOY_ENV", "development"), patch.object(bot, "VERSION", "0.1.0+5"), patch.dict("os.environ", {"DEV_CHAT_ID": "2201809732"}, clear=False), patch.object(bot, "get_deploy_notice_version", return_value="0.1.0+4"), patch.object(bot, "set_deploy_notice_version"), patch.object(bot.asyncio, "to_thread", new=AsyncMock(side_effect=["0.1.0+4", None, "0.1.0+5"])), ): await bot.notify_development_deploy(fake_bot) await bot.notify_development_deploy(fake_bot) fake_bot.send_message.assert_awaited_once_with(chat_id=2201809732, text="🚀 Бот обновлён\nВерсия: 0.1.0+5") async def test_does_not_send_outside_development(self): fake_bot = AsyncMock() with patch.object(bot, "DEPLOY_ENV", "production"): await bot.notify_development_deploy(fake_bot) fake_bot.send_message.assert_not_awaited() async def test_deploy_notice_does_not_block_if_test_api_hangs(self): fake_bot = AsyncMock() fake_bot.send_message.side_effect = asyncio.TimeoutError with ( patch.object(bot, "DEPLOY_ENV", "development"), patch.dict("os.environ", {"DEV_CHAT_ID": "2201809732"}, clear=False), patch.object(bot, "get_deploy_notice_version", return_value="old"), ): await bot.notify_development_deploy(fake_bot) fake_bot.send_message.assert_awaited_once() class HelpRoutingTests(unittest.TestCase): def test_commands_are_routed_in_command_handler(self): self.assertEqual(bot.HELP_COMMANDS, {"help", "помощь"}) from pathlib import Path source = Path(bot.__file__).read_text(encoding="utf-8") self.assertIn("async def handle_command_text", source) self.assertIn("if command == \"/help\"", source) self.assertIn("elif command == \"/start\"", source) self.assertIn("elif command == \"/clear\"", source) self.assertIn("elif command == \"/idea\"", source) self.assertNotIn("idea_voice", source) def test_help_rich_message_contains_usage_scenarios(self): from help_message import HELP_RICH_HTML, rich_to_plain text = rich_to_plain(HELP_RICH_HTML) self.assertIn("/clear", text) self.assertIn("/idea", text) self.assertNotIn("/idea_voice", text) for term in ("пост", "голосовое", "скриншот", "реплаем", "/help"): self.assertIn(term, text) class OnboardingTests(unittest.IsolatedAsyncioTestCase): async def test_start_sends_welcome_rich_then_help_command(self): message = AsyncMock() message.from_user.id = 42 message.message_thread_id = None calls: list[str] = [] async def fake_rich(_message, html, _stage): calls.append(html) with ( patch.object(bot, "notify_development_deploy", new=AsyncMock()), patch.object(bot, "ensure_subscription"), patch.object(bot.asyncio, "to_thread", new=AsyncMock()), patch.object(bot, "send_rich_message", new=fake_rich), ): await bot.start(message) self.assertEqual(calls, [bot.WELCOME_RICH_HTML, bot.HELP_RICH_HTML]) async def test_start_help_step_reuses_help_command(self): from pathlib import Path source = Path(bot.__file__).read_text(encoding="utf-8") start_body = source.split("async def start(message: Message) -> None:")[1].split("\nasync def ")[0] self.assertIn("await help_command(message)", start_body) def test_welcome_message_has_examples_and_help_handoff(self): from help_message import WELCOME_RICH_HTML, rich_to_plain text = rich_to_plain(WELCOME_RICH_HTML) for term in ("Привет", "Напиши пост", "скриншот"): self.assertIn(term, text) if __name__ == "__main__": unittest.main()