Переписал почти все тесты

feat: улучшено логирование и обработка скорингов в PostService и RagApiClient

- Добавлены отладочные сообщения для передачи скорингов в функции обработки постов.
- Обновлено логирование успешного получения скорингов из RAG API с дополнительной информацией.
- Оптимизирована обработка скорингов в функции get_text_message для улучшения отладки.
- Обновлены тесты для проверки новых функциональных возможностей и обработки ошибок.
This commit is contained in:
2026-01-30 00:55:47 +03:00
parent e87f4af82f
commit a5faa4bdc6
27 changed files with 4320 additions and 8 deletions

209
tests/test_rag_client.py Normal file
View File

@@ -0,0 +1,209 @@
"""
Тесты для 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