Files
fckbot/tests/integration/test_commands.py
2026-06-30 22:39:15 +00:00

303 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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()