- Add ADR 001 for Hybrid Search Architecture - Implement Phase 1 (Exact Match) and Phase 2 (Semantic Fallback) in ChromaStore - Wrap blocking ChromaDB calls in asyncio.to_thread - Update IVectorStore interface to support category filtering and thresholds - Add comprehensive tests for hybrid search logic
169 lines
5.4 KiB
Python
169 lines
5.4 KiB
Python
import uuid
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from aiogram.types import Message, InlineKeyboardMarkup
|
|
from aiogram.filters import CommandObject
|
|
from datetime import datetime
|
|
|
|
from src.bot.handlers import get_router
|
|
from src.processor.dto import EnrichedNewsItemDTO
|
|
|
|
@pytest.fixture
|
|
def mock_storage():
|
|
return AsyncMock()
|
|
|
|
@pytest.fixture
|
|
def mock_processor():
|
|
processor = MagicMock()
|
|
processor.get_info.return_value = {"model": "test-model"}
|
|
return processor
|
|
|
|
@pytest.fixture
|
|
def allowed_chat_id():
|
|
return "123456789"
|
|
|
|
@pytest.fixture
|
|
def router(mock_storage, mock_processor, allowed_chat_id):
|
|
return get_router(mock_storage, mock_processor, allowed_chat_id)
|
|
|
|
def get_handler(router, callback_name):
|
|
for handler in router.message.handlers:
|
|
if handler.callback.__name__ == callback_name:
|
|
return handler.callback
|
|
raise ValueError(f"Handler {callback_name} not found")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hottest_handler_success(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test that /hottest command calls get_top_ranked and returns a list of items.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
message.answer = AsyncMock()
|
|
|
|
mock_items = [
|
|
EnrichedNewsItemDTO(
|
|
title=f"Hot News {i}",
|
|
url=f"https://example.com/{i}",
|
|
content_text=f"Content {i}",
|
|
source="Source",
|
|
timestamp=datetime.now(),
|
|
relevance_score=10-i,
|
|
summary_ru=f"Сводка {i}",
|
|
anomalies_detected=[],
|
|
category="Tech"
|
|
) for i in range(3)
|
|
]
|
|
mock_storage.get_top_ranked.return_value = mock_items
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='hottest', args=None)
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=10, category=None)
|
|
message.answer.assert_called_once()
|
|
|
|
args, kwargs = message.answer.call_args
|
|
assert "Top 3 Hottest Trends:" in args[0]
|
|
assert "reply_markup" in kwargs
|
|
assert isinstance(kwargs["reply_markup"], InlineKeyboardMarkup)
|
|
|
|
# Check if all 3 items are in the markup
|
|
markup = kwargs["reply_markup"]
|
|
assert len(markup.inline_keyboard) == 3
|
|
|
|
# Check if icons and scores are present
|
|
button_text = markup.inline_keyboard[0][0].text
|
|
assert "🔥" in button_text
|
|
assert "[10/10]" in button_text
|
|
assert "Hot News 0" in button_text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hottest_handler_empty(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test that /hottest command handles empty results correctly.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
message.answer = AsyncMock()
|
|
|
|
mock_storage.get_top_ranked.return_value = []
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='hottest', args=None)
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=10, category=None)
|
|
message.answer.assert_called_once_with("No hot trends found yet.")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hottest_handler_custom_limit(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test that /hottest command with custom limit correctly passes it to storage.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
message.answer = AsyncMock()
|
|
|
|
mock_storage.get_top_ranked.return_value = []
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='hottest', args='25')
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=25, category=None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hottest_handler_max_limit(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test that /hottest command enforces maximum limit.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
message.answer = AsyncMock()
|
|
|
|
mock_storage.get_top_ranked.return_value = []
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='hottest', args='1000')
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=50, category=None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_command_hottest_handler_invalid_limit(router, mock_storage, allowed_chat_id):
|
|
"""
|
|
Test that /hottest command handles invalid limit by falling back to default.
|
|
"""
|
|
# 1. Arrange
|
|
handler = get_handler(router, "command_hottest_handler")
|
|
message = AsyncMock()
|
|
message.chat = MagicMock()
|
|
message.chat.id = int(allowed_chat_id)
|
|
message.answer = AsyncMock()
|
|
|
|
mock_storage.get_top_ranked.return_value = []
|
|
|
|
# 2. Act
|
|
command = CommandObject(prefix='/', command='hottest', args='invalid')
|
|
await handler(message=message, command=command)
|
|
|
|
# 3. Assert
|
|
mock_storage.get_top_ranked.assert_called_once_with(limit=10, category='invalid')
|