# tests/integration/test_commands.py """ Интеграционные тесты для переработанных команд бота. Проверяют, что каждая команда вызывает правильный метод RAGClient с корректными аргументами. Используются моки для RAGClient, чтобы изолировать тесты от реального HTTP-сервера. """ import pytest from unittest.mock import AsyncMock, MagicMock, patch # Импортируем все классы команд from core.commands.clear import ClearCommand from core.commands.kb import KbCommand from core.commands.global_remove import GlobalRemoveCommand from core.commands.other import ResetCommand from core.commands.template import TemplateSaveCommand, TemplateListCommand, TemplateDeleteCommand from core.commands.stats import StatsCommand, StatusCommand, CleanCommand from core.commands.create import CreateCommand from core.commands.expert import SummaryCommand, MetricsCommand from core.commands.learn import LearnCommand, StopLearnCommand, GlobalLearnCommand from core.commands.info import InfoCommand from core.commands.help import HelpCommand from core.commands.base import Command @pytest.fixture def mock_bot(): """Создаёт мок бота с мокнутым rag_client и минимальными атрибутами.""" bot = MagicMock() bot.rag_client = AsyncMock() # Для команд, использующих config bot.config = MagicMock() bot.config.name = "testbot" bot.config.admin_jids = ["admin@domain"] # Для команд, использующих learning_mode bot.learning_mode = {} # Для команд, использующих _last_file_path bot._last_file_path = {} # Для команд, использующих last_file_texts bot.last_file_texts = {} # Для старта времени (stats) bot.start_time = MagicMock() # Для команды info (не использует rag_client) return bot @pytest.fixture def mock_msg(): """Создаёт мок XMPP сообщения с методом reply.""" msg = MagicMock() msg.reply.return_value = MagicMock() return msg # ---------- Тесты для команды !clear ---------- @pytest.mark.asyncio async def test_clear_command(mock_bot, mock_msg): cmd = ClearCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.clear.assert_called_once_with("user@domain", None, False) # Проверка с комнатой mock_bot.rag_client.clear.reset_mock() await cmd.execute(mock_msg, "", "user@domain", "room@domain") mock_bot.rag_client.clear.assert_called_once_with("user@domain", "room@domain", False) # ---------- Тесты для команды !kb ---------- @pytest.mark.asyncio async def test_kb_command(mock_bot, mock_msg): mock_bot.rag_client.list_documents.return_value = { "documents": [ {"source_name": "doc1.docx", "is_global": False}, {"source_name": "doc2.pdf", "is_global": True} ] } cmd = KbCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.list_documents.assert_called_once_with("user@domain", None) # Проверка с комнатой mock_bot.rag_client.list_documents.reset_mock() await cmd.execute(mock_msg, "", "user@domain", "room@domain") mock_bot.rag_client.list_documents.assert_called_once_with("user@domain", "room@domain") # ---------- Тесты для команды !global_remove ---------- @pytest.mark.asyncio async def test_global_remove_command(mock_bot, mock_msg): # Проверка прав (админ) mock_bot.config.admin_jids = ["admin@domain"] cmd = GlobalRemoveCommand(mock_bot) await cmd.execute(mock_msg, '!global_remove "ГОСТ Р 12345"', "admin@domain", None) mock_bot.rag_client.delete_global.assert_called_once_with("ГОСТ Р 12345") # Проверка без прав mock_bot.rag_client.delete_global.reset_mock() await cmd.execute(mock_msg, '!global_remove "ГОСТ Р 12345"', "user@domain", None) mock_bot.rag_client.delete_global.assert_not_called() # Проверка в комнате (должна отклонить) mock_bot.rag_client.delete_global.reset_mock() await cmd.execute(mock_msg, '!global_remove "ГОСТ Р 12345"', "admin@domain", "room@domain") mock_bot.rag_client.delete_global.assert_not_called() # ---------- Тесты для команды !reset ---------- @pytest.mark.asyncio async def test_reset_command(mock_bot, mock_msg): cmd = ResetCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.reset_history.assert_called_once_with("user@domain", None) mock_bot.rag_client.reset_history.reset_mock() await cmd.execute(mock_msg, "", "user@domain", "room@domain") mock_bot.rag_client.reset_history.assert_called_once_with("user@domain", "room@domain") # ---------- Тесты для команд шаблонов ---------- @pytest.mark.asyncio async def test_template_save_command(mock_bot, mock_msg): # Подготовка: бот должен иметь _last_file_path mock_bot._last_file_path = {("user@domain", "room@domain"): "/tmp/test.docx"} cmd = TemplateSaveCommand(mock_bot) await cmd.execute(mock_msg, '!template_save "Шаблон"', "user@domain", "room@domain") # Проверяем, что вызывается template_save с правильными аргументами mock_bot.rag_client.template_save.assert_called_once() args, kwargs = mock_bot.rag_client.template_save.call_args assert args[0] == "room@domain" # room_jid assert args[1] == "Шаблон" # name assert args[2] == "/tmp/test.docx" # file_path # file_hash проверять не будем, т.к. он вычисляется @pytest.mark.asyncio async def test_template_list_command(mock_bot, mock_msg): cmd = TemplateListCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", "room@domain") mock_bot.rag_client.template_list.assert_called_once_with("room@domain") @pytest.mark.asyncio async def test_template_delete_command(mock_bot, mock_msg): cmd = TemplateDeleteCommand(mock_bot) await cmd.execute(mock_msg, '!template_delete "Шаблон"', "user@domain", "room@domain") mock_bot.rag_client.template_delete.assert_called_once_with("room@domain", "Шаблон") # ---------- Тесты для команд статистики ---------- @pytest.mark.asyncio async def test_stats_command(mock_bot, mock_msg): # Проверка прав (админ) mock_bot.config.admin_jids = ["admin@domain"] mock_bot.rag_client.get_stats.return_value = {"documents": 42, "uptime": 123} cmd = StatsCommand(mock_bot) await cmd.execute(mock_msg, "", "admin@domain", None) mock_bot.rag_client.get_stats.assert_called_once() # Без прав mock_bot.rag_client.get_stats.reset_mock() await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.get_stats.assert_not_called() @pytest.mark.asyncio async def test_status_command(mock_bot, mock_msg): mock_bot.config.admin_jids = ["admin@domain"] mock_bot.rag_client.health_check.return_value = True cmd = StatusCommand(mock_bot) await cmd.execute(mock_msg, "", "admin@domain", None) mock_bot.rag_client.health_check.assert_called_once() mock_bot.rag_client.health_check.reset_mock() await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.health_check.assert_not_called() @pytest.mark.asyncio async def test_clean_command(mock_bot, mock_msg): mock_bot.config.admin_jids = ["admin@domain"] # clean не использует rag_client, просто проверяет права и очищает temp_dir # Проверим, что команда не вызывает rag_client cmd = CleanCommand(mock_bot) await cmd.execute(mock_msg, "", "admin@domain", None) # Никаких вызовов rag_client не должно быть mock_bot.rag_client.assert_not_called() # ---------- Тесты для команды !create ---------- @pytest.mark.asyncio async def test_create_command(mock_bot, mock_msg): mock_bot.rag_client.generate_document.return_value = {"document": "/tmp/out.docx"} cmd = CreateCommand(mock_bot) await cmd.execute(mock_msg, '!create "Шаблон"', "user@domain", "room@domain") mock_bot.rag_client.generate_document.assert_called_once_with("Шаблон", "user@domain", "room@domain") # ---------- Тесты для экспертных команд ---------- @pytest.mark.asyncio async def test_summary_command(mock_bot, mock_msg): mock_bot.rag_client.query.return_value = {"answer": "Суммаризация документа..."} mock_bot.last_file_texts = {"user@domain": "Текст документа"} cmd = SummaryCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.query.assert_called_once_with( query="Сделай суммаризацию последнего документа", user_jid="user@domain", room_jid=None, prompts=mock_bot.prompts, intent_override="SUMMARY", last_file_path=None, last_file_text="Текст документа" ) @pytest.mark.asyncio async def test_metrics_command(mock_bot, mock_msg): mock_bot.rag_client.query.return_value = {"answer": "KPI: ..."} cmd = MetricsCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_bot.rag_client.query.assert_called_once_with( query="Извлеки все KPI из документов", user_jid="user@domain", room_jid=None, prompts=mock_bot.prompts, intent_override="METRICS", last_file_path=None, last_file_text=None ) # ---------- Тесты для команд обучения ---------- @pytest.mark.asyncio async def test_learn_command(mock_bot, mock_msg): cmd = LearnCommand(mock_bot) # Личный чат, личное обучение await cmd.execute(mock_msg, '!learn', "user@domain", None) assert mock_bot.learning_mode.get("user@domain") == ("personal", None) # Глобальное обучение (админ) mock_bot.config.admin_jids = ["admin@domain"] await cmd.execute(mock_msg, '!learn --global', "admin@domain", None) assert mock_bot.learning_mode.get("admin@domain") == ("global", None) # Комнатное обучение await cmd.execute(mock_msg, '!learn', "user@domain", "room@domain") assert mock_bot.learning_mode.get("user@domain") == ("room", "room@domain") # С --title await cmd.execute(mock_msg, '!learn --title "Мой заголовок"', "user@domain", None) # Проверим, что заголовок не влияет на режим (он сохраняется в learning_mode) # Но команда отправляет сообщение, мы не проверяем содержимое @pytest.mark.asyncio async def test_stop_learn_command(mock_bot, mock_msg): mock_bot.learning_mode = {"user@domain": ("personal", None)} cmd = StopLearnCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) assert "user@domain" not in mock_bot.learning_mode @pytest.mark.asyncio async def test_global_learn_command(mock_bot, mock_msg): mock_bot.config.admin_jids = ["admin@domain"] cmd = GlobalLearnCommand(mock_bot) await cmd.execute(mock_msg, "", "admin@domain", None) assert mock_bot.learning_mode.get("admin@domain") == ("global", None) # Без прав mock_bot.learning_mode = {} await cmd.execute(mock_msg, "", "user@domain", None) assert "user@domain" not in mock_bot.learning_mode # В комнате await cmd.execute(mock_msg, "", "admin@domain", "room@domain") assert mock_bot.learning_mode.get("admin@domain") == ("global", None) # не изменилось # ---------- Тесты для info и help (не используют rag_client) ---------- @pytest.mark.asyncio async def test_info_command(mock_bot, mock_msg): cmd = InfoCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) # Просто проверяем, что отправлено сообщение mock_msg.reply.assert_called_once() @pytest.mark.asyncio async def test_help_command(mock_bot, mock_msg): # Подготовка: бот должен иметь команды mock_bot.commands = { "!clear": ClearCommand(mock_bot), "!help": HelpCommand(mock_bot), } cmd = HelpCommand(mock_bot) await cmd.execute(mock_msg, "", "user@domain", None) mock_msg.reply.assert_called_once() # Проверка справки по конкретной команде mock_msg.reply.reset_mock() await cmd.execute(mock_msg, "!help !clear", "user@domain", None) mock_msg.reply.assert_called_once()