feat: улучшено логирование и обработка скорингов в PostService и RagApiClient - Добавлены отладочные сообщения для передачи скорингов в функции обработки постов. - Обновлено логирование успешного получения скорингов из RAG API с дополнительной информацией. - Оптимизирована обработка скорингов в функции get_text_message для улучшения отладки. - Обновлены тесты для проверки новых функциональных возможностей и обработки ошибок.
210 lines
8.6 KiB
Python
210 lines
8.6 KiB
Python
"""
|
||
Тесты для helper_bot.services.scoring.rag_client (RagApiClient).
|
||
"""
|
||
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from helper_bot.services.scoring.exceptions import (
|
||
InsufficientExamplesError,
|
||
ScoringError,
|
||
TextTooShortError,
|
||
)
|
||
from helper_bot.services.scoring.rag_client import RagApiClient
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestRagApiClientInit:
|
||
"""Тесты инициализации RagApiClient."""
|
||
|
||
def test_init_strips_trailing_slash(self):
|
||
"""api_url без trailing slash."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
client = RagApiClient(api_url="http://api/v1/", api_key="key")
|
||
assert client.api_url == "http://api/v1"
|
||
|
||
def test_source_name_is_rag(self):
|
||
"""source_name возвращает 'rag'."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
client = RagApiClient(api_url="http://api", api_key="key")
|
||
assert client.source_name == "rag"
|
||
|
||
def test_is_enabled_true_by_default(self):
|
||
"""is_enabled True по умолчанию."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
client = RagApiClient(api_url="http://api", api_key="key")
|
||
assert client.is_enabled is True
|
||
|
||
def test_is_enabled_false_when_disabled(self):
|
||
"""is_enabled False при enabled=False."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
client = RagApiClient(api_url="http://api", api_key="key", enabled=False)
|
||
assert client.is_enabled is False
|
||
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
class TestRagApiClientCalculateScore:
|
||
"""Тесты calculate_score."""
|
||
|
||
@pytest.fixture
|
||
def client(self):
|
||
"""Клиент с замоканным _client."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
c = RagApiClient(api_url="http://rag/api", api_key="key")
|
||
c._client = MagicMock()
|
||
return c
|
||
|
||
async def test_disabled_raises_scoring_error(self, client):
|
||
"""При отключённом клиенте выбрасывается ScoringError."""
|
||
client._enabled = False
|
||
with pytest.raises(ScoringError, match="отключен"):
|
||
await client.calculate_score("text")
|
||
|
||
async def test_empty_text_raises_text_too_short(self, client):
|
||
"""Пустой текст — TextTooShortError."""
|
||
with pytest.raises(TextTooShortError, match="пустой"):
|
||
await client.calculate_score(" ")
|
||
|
||
async def test_success_200_returns_scoring_result(self, client):
|
||
"""Успешный ответ 200 возвращает ScoringResult."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
mock_response.json.return_value = {
|
||
"rag_score": 0.85,
|
||
"rag_confidence": 0.9,
|
||
"rag_score_pos_only": 0.82,
|
||
"meta": {"model": "test-model"},
|
||
}
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
result = await client.calculate_score("Post text")
|
||
|
||
assert result.score == 0.85
|
||
assert result.source == "rag"
|
||
assert result.model == "test-model"
|
||
assert result.confidence == 0.9
|
||
assert result.metadata["rag_score_pos_only"] == 0.82
|
||
|
||
async def test_400_insufficient_raises_insufficient_examples(self, client):
|
||
"""400 с 'недостаточно' в detail — InsufficientExamplesError."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 400
|
||
mock_response.json.return_value = {"detail": "Недостаточно примеров"}
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
with pytest.raises(InsufficientExamplesError):
|
||
await client.calculate_score("text")
|
||
|
||
async def test_400_short_raises_text_too_short(self, client):
|
||
"""400 с 'коротк' в detail — TextTooShortError."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 400
|
||
mock_response.json.return_value = {"detail": "Текст слишком короткий"}
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
with pytest.raises(TextTooShortError):
|
||
await client.calculate_score("ab")
|
||
|
||
async def test_401_raises_scoring_error(self, client):
|
||
"""401 — ScoringError про аутентификацию."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 401
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
with pytest.raises(ScoringError, match="аутентификации"):
|
||
await client.calculate_score("text")
|
||
|
||
async def test_500_raises_scoring_error(self, client):
|
||
"""5xx — ScoringError."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 500
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
with pytest.raises(ScoringError, match="сервера"):
|
||
await client.calculate_score("text")
|
||
|
||
async def test_timeout_raises_scoring_error(self, client):
|
||
"""Таймаут запроса — ScoringError."""
|
||
class FakeTimeoutException(Exception):
|
||
pass
|
||
|
||
with patch(
|
||
"helper_bot.services.scoring.rag_client.httpx"
|
||
) as mock_httpx:
|
||
mock_httpx.TimeoutException = FakeTimeoutException
|
||
client._client.post = AsyncMock(
|
||
side_effect=FakeTimeoutException("timeout")
|
||
)
|
||
with pytest.raises(ScoringError, match="Таймаут"):
|
||
await client.calculate_score("text")
|
||
|
||
|
||
@pytest.mark.unit
|
||
@pytest.mark.asyncio
|
||
class TestRagApiClientExamplesAndStats:
|
||
"""Тесты add_positive_example, add_negative_example, get_stats, get_stats_sync."""
|
||
|
||
@pytest.fixture
|
||
def client(self):
|
||
"""Клиент с замоканным _client."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
c = RagApiClient(api_url="http://rag/api", api_key="key")
|
||
c._client = MagicMock()
|
||
return c
|
||
|
||
async def test_add_positive_example_disabled_returns_early(self, client):
|
||
"""При отключённом клиенте add_positive_example ничего не делает."""
|
||
client._enabled = False
|
||
await client.add_positive_example("text")
|
||
client._client.post.assert_not_called()
|
||
|
||
async def test_add_positive_example_success(self, client):
|
||
"""Успешное добавление положительного примера."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
client._client.post = AsyncMock(return_value=mock_response)
|
||
|
||
await client.add_positive_example("Good post")
|
||
|
||
client._client.post.assert_called_once()
|
||
call_kwargs = client._client.post.call_args[1]
|
||
assert call_kwargs["json"] == {"text": "Good post"}
|
||
|
||
async def test_add_positive_example_test_mode_sends_header(self):
|
||
"""При test_mode=True отправляется заголовок X-Test-Mode."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
c = RagApiClient(api_url="http://rag", api_key="k", test_mode=True)
|
||
c._client = MagicMock()
|
||
c._client.post = AsyncMock(return_value=MagicMock(status_code=200))
|
||
await c.add_positive_example("t")
|
||
call_kwargs = c._client.post.call_args[1]
|
||
assert call_kwargs.get("headers", {}).get("X-Test-Mode") == "true"
|
||
|
||
async def test_get_stats_200_returns_json(self, client):
|
||
"""get_stats при 200 возвращает json."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
mock_response.json.return_value = {"total": 10}
|
||
client._client.get = AsyncMock(return_value=mock_response)
|
||
|
||
result = await client.get_stats()
|
||
|
||
assert result == {"total": 10}
|
||
|
||
async def test_get_stats_disabled_returns_empty(self, client):
|
||
"""При отключённом клиенте get_stats возвращает {}."""
|
||
client._enabled = False
|
||
result = await client.get_stats()
|
||
assert result == {}
|
||
|
||
def test_get_stats_sync_returns_dict(self):
|
||
"""get_stats_sync возвращает словарь с enabled, api_url, timeout."""
|
||
with patch("helper_bot.services.scoring.rag_client.httpx.AsyncClient"):
|
||
client = RagApiClient(api_url="http://api", api_key="k", timeout=15)
|
||
result = client.get_stats_sync()
|
||
assert result["enabled"] is True
|
||
assert result["api_url"] == "http://api"
|
||
assert result["timeout"] == 15
|