diff --git a/tests/integration/test_commands.py b/tests/integration/test_commands.py index b9df1a2..a6c6af7 100644 --- a/tests/integration/test_commands.py +++ b/tests/integration/test_commands.py @@ -1,18 +1,303 @@ +# tests/integration/test_commands.py """ -Интеграционные тесты для команд бота (проверяют вызовы RAGClient). +Интеграционные тесты для переработанных команд бота. +Проверяют, что каждая команда вызывает правильный метод RAGClient с корректными аргументами. +Используются моки для RAGClient, чтобы изолировать тесты от реального HTTP-сервера. """ import pytest -from unittest.mock import AsyncMock, MagicMock -from core.commands.clear import ClearCommand +from unittest.mock import AsyncMock, MagicMock, patch -@pytest.mark.asyncio -async def test_clear_command(): +# Импортируем все классы команд +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() - bot.rag_client.clear = AsyncMock(return_value={"status": "ok"}) - cmd = ClearCommand(bot) + # Для команд, использующих 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() - await cmd.execute(msg, "", "user@domain", None) - bot.rag_client.clear.assert_called_once_with("user@domain", None, False) \ No newline at end of file + 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() \ No newline at end of file