Files
telegram-helper-bot/tests/test_scoring_services.py
Andrey 3d6b4353f9
All checks were successful
CI pipeline / Test & Code Quality (push) Successful in 34s
Refactor imports across multiple files to improve code organization and readability.
2026-02-28 23:24:25 +03:00

425 lines
15 KiB
Python
Raw 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.
"""
Тесты для сервисов ML-скоринга постов.
"""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# Импорты для тестирования базовых классов
from helper_bot.services.scoring.base import CombinedScore, ScoringResult
from helper_bot.services.scoring.exceptions import (
InsufficientExamplesError,
ScoringError,
TextTooShortError,
)
class TestScoringResult:
"""Тесты для ScoringResult."""
def test_create_valid_score(self):
"""Тест создания валидного результата."""
result = ScoringResult(
score=0.75,
source="rag",
model="test-model",
)
assert result.score == 0.75
assert result.source == "rag"
assert result.model == "test-model"
def test_score_validation_lower_bound(self):
"""Тест валидации нижней границы скора."""
with pytest.raises(ValueError):
ScoringResult(score=-0.1, source="test", model="test")
def test_score_validation_upper_bound(self):
"""Тест валидации верхней границы скора."""
with pytest.raises(ValueError):
ScoringResult(score=1.1, source="test", model="test")
def test_to_dict(self):
"""Тест преобразования в словарь."""
result = ScoringResult(
score=0.7534,
source="rag",
model="test-model",
confidence=0.85,
timestamp=1234567890,
)
d = result.to_dict()
assert d["score"] == 0.7534 # Округлено до 4 знаков
assert d["model"] == "test-model"
assert d["ts"] == 1234567890
assert d["confidence"] == 0.85
def test_from_dict(self):
"""Тест создания из словаря."""
data = {
"score": 0.75,
"model": "test-model",
"ts": 1234567890,
"confidence": 0.9,
}
result = ScoringResult.from_dict("rag", data)
assert result.score == 0.75
assert result.source == "rag"
assert result.model == "test-model"
assert result.timestamp == 1234567890
assert result.confidence == 0.9
class TestCombinedScore:
"""Тесты для CombinedScore."""
def test_empty_combined_score(self):
"""Тест пустого объединенного скора."""
score = CombinedScore()
assert score.deepseek is None
assert score.rag is None
assert score.deepseek_score is None
assert score.rag_score is None
assert not score.has_any_score()
def test_combined_score_with_rag(self):
"""Тест объединенного скора с RAG."""
rag_result = ScoringResult(score=0.8, source="rag", model="rubert")
score = CombinedScore(rag=rag_result)
assert score.rag_score == 0.8
assert score.deepseek_score is None
assert score.has_any_score()
def test_combined_score_with_both(self):
"""Тест объединенного скора с обоими сервисами."""
rag_result = ScoringResult(score=0.8, source="rag", model="rubert")
deepseek_result = ScoringResult(
score=0.7, source="deepseek", model="deepseek-chat"
)
score = CombinedScore(rag=rag_result, deepseek=deepseek_result)
assert score.rag_score == 0.8
assert score.deepseek_score == 0.7
assert score.has_any_score()
def test_to_json_dict(self):
"""Тест преобразования в JSON словарь."""
rag_result = ScoringResult(
score=0.8, source="rag", model="rubert", timestamp=123
)
deepseek_result = ScoringResult(
score=0.7, source="deepseek", model="deepseek-chat", timestamp=456
)
score = CombinedScore(rag=rag_result, deepseek=deepseek_result)
d = score.to_json_dict()
assert "rag" in d
assert "deepseek" in d
assert d["rag"]["score"] == 0.8
assert d["deepseek"]["score"] == 0.7
# Проверяем что это валидный JSON
json_str = json.dumps(d)
assert json_str
class TestVectorStore:
"""Тесты для VectorStore (требует numpy)."""
@pytest.fixture
def vector_store(self):
"""Создает VectorStore для тестов."""
try:
import numpy as np
from helper_bot.services.scoring.vector_store import VectorStore
return VectorStore(vector_dim=768, max_examples=100)
except ImportError:
pytest.skip("numpy не установлен")
def test_add_positive_example(self, vector_store):
"""Тест добавления положительного примера."""
import numpy as np
vector = np.random.randn(768).astype(np.float32)
result = vector_store.add_positive(vector, "hash1")
assert result is True
assert vector_store.positive_count == 1
def test_add_duplicate_example(self, vector_store):
"""Тест добавления дубликата."""
import numpy as np
vector = np.random.randn(768).astype(np.float32)
vector_store.add_positive(vector, "hash1")
result = vector_store.add_positive(vector, "hash1") # Дубликат
assert result is False
assert vector_store.positive_count == 1
def test_max_examples_limit(self, vector_store):
"""Тест ограничения максимального количества примеров."""
import numpy as np
# Добавляем больше чем max_examples
for i in range(150):
vector = np.random.randn(768).astype(np.float32)
vector_store.add_positive(vector, f"hash_{i}")
assert vector_store.positive_count == 100 # max_examples
def test_calculate_similarity_no_examples(self, vector_store):
"""Тест расчета скора без примеров."""
import numpy as np
vector = np.random.randn(768).astype(np.float32)
with pytest.raises(InsufficientExamplesError):
vector_store.calculate_similarity_score(vector)
def test_calculate_similarity_with_examples(self, vector_store):
"""Тест расчета скора с примерами."""
import numpy as np
# Добавляем положительные примеры
for i in range(10):
vector = np.random.randn(768).astype(np.float32)
vector_store.add_positive(vector, f"pos_{i}")
# Добавляем отрицательные примеры
for i in range(10):
vector = np.random.randn(768).astype(np.float32)
vector_store.add_negative(vector, f"neg_{i}")
# Рассчитываем скор для нового вектора
test_vector = np.random.randn(768).astype(np.float32)
score, confidence = vector_store.calculate_similarity_score(test_vector)
assert 0.0 <= score <= 1.0
assert 0.0 <= confidence <= 1.0
def test_compute_text_hash(self, vector_store):
"""Тест вычисления хеша текста."""
from helper_bot.services.scoring.vector_store import VectorStore
hash1 = VectorStore.compute_text_hash("Привет мир")
hash2 = VectorStore.compute_text_hash("Привет мир")
hash3 = VectorStore.compute_text_hash("Другой текст")
assert hash1 == hash2
assert hash1 != hash3
class TestDeepSeekService:
"""Тесты для DeepSeekService."""
@pytest.fixture
def deepseek_service(self):
"""Создает DeepSeekService для тестов."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
return DeepSeekService(
api_key="test_key",
enabled=True,
timeout=5,
)
def test_service_disabled_without_key(self):
"""Тест отключения сервиса без API ключа."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
service = DeepSeekService(api_key=None, enabled=True)
assert service.is_enabled is False
def test_parse_score_response_valid(self, deepseek_service):
"""Тест парсинга валидного ответа."""
assert deepseek_service._parse_score_response("0.75") == 0.75
assert deepseek_service._parse_score_response("0.5") == 0.5
assert deepseek_service._parse_score_response("1.0") == 1.0
assert deepseek_service._parse_score_response("0") == 0.0
def test_parse_score_response_with_quotes(self, deepseek_service):
"""Тест парсинга ответа с кавычками."""
assert deepseek_service._parse_score_response('"0.75"') == 0.75
assert deepseek_service._parse_score_response("'0.8'") == 0.8
def test_parse_score_response_with_text(self, deepseek_service):
"""Тест парсинга ответа с текстом."""
# Сервис должен найти число в тексте
assert deepseek_service._parse_score_response("Score: 0.75") == 0.75
def test_clean_text(self, deepseek_service):
"""Тест очистки текста."""
assert deepseek_service._clean_text(" hello world ") == "hello world"
assert deepseek_service._clean_text("^") == ""
assert deepseek_service._clean_text("") == ""
@pytest.mark.asyncio
async def test_calculate_score_disabled(self):
"""Тест расчета скора при отключенном сервисе."""
from helper_bot.services.scoring.deepseek_service import DeepSeekService
service = DeepSeekService(api_key=None, enabled=False)
with pytest.raises(ScoringError):
await service.calculate_score("Test text")
@pytest.mark.asyncio
async def test_calculate_score_short_text(self, deepseek_service):
"""Тест расчета скора для короткого текста."""
with pytest.raises(TextTooShortError):
await deepseek_service.calculate_score("ab")
class TestScoringManager:
"""Тесты для ScoringManager."""
@pytest.fixture
def mock_rag_service(self):
"""Создает мок RAG сервиса."""
mock = AsyncMock()
mock.is_enabled = True
mock.calculate_score = AsyncMock(
return_value=ScoringResult(
score=0.8,
source="rag",
model="rubert",
)
)
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock
@pytest.fixture
def mock_deepseek_service(self):
"""Создает мок DeepSeek сервиса."""
mock = AsyncMock()
mock.is_enabled = True
mock.calculate_score = AsyncMock(
return_value=ScoringResult(
score=0.7,
source="deepseek",
model="deepseek-chat",
)
)
mock.add_positive_example = AsyncMock()
mock.add_negative_example = AsyncMock()
return mock
@pytest.mark.asyncio
async def test_score_post_both_services(
self, mock_rag_service, mock_deepseek_service
):
"""Тест скоринга с обоими сервисами."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
result = await manager.score_post("Тестовый пост")
assert result.rag_score == 0.8
assert result.deepseek_score == 0.7
assert result.has_any_score()
@pytest.mark.asyncio
async def test_score_post_rag_only(self, mock_rag_service):
"""Тест скоринга только с RAG."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_client=mock_rag_service,
deepseek_service=None,
)
result = await manager.score_post("Тестовый пост")
assert result.rag_score == 0.8
assert result.deepseek_score is None
@pytest.mark.asyncio
async def test_score_post_empty_text(self, mock_rag_service):
"""Тест скоринга пустого текста."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(rag_client=mock_rag_service)
result = await manager.score_post("")
assert not result.has_any_score()
mock_rag_service.calculate_score.assert_not_called()
@pytest.mark.asyncio
async def test_score_post_service_error(
self, mock_rag_service, mock_deepseek_service
):
"""Тест обработки ошибки сервиса."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
# RAG выбрасывает ошибку
mock_rag_service.calculate_score = AsyncMock(
side_effect=Exception("Test error")
)
manager = ScoringManager(
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
result = await manager.score_post("Тестовый пост")
# DeepSeek должен вернуть результат
assert result.deepseek_score == 0.7
# RAG должен быть None с ошибкой
assert result.rag_score is None
assert "rag" in result.errors
@pytest.mark.asyncio
async def test_on_post_published(self, mock_rag_service, mock_deepseek_service):
"""Тест обучения на опубликованном посте."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
await manager.on_post_published("Опубликованный пост")
mock_rag_service.add_positive_example.assert_called_once_with(
"Опубликованный пост"
)
mock_deepseek_service.add_positive_example.assert_called_once_with(
"Опубликованный пост"
)
@pytest.mark.asyncio
async def test_on_post_declined(self, mock_rag_service, mock_deepseek_service):
"""Тест обучения на отклоненном посте."""
from helper_bot.services.scoring.scoring_manager import ScoringManager
manager = ScoringManager(
rag_client=mock_rag_service,
deepseek_service=mock_deepseek_service,
)
await manager.on_post_declined("Отклоненный пост")
mock_rag_service.add_negative_example.assert_called_once_with(
"Отклоненный пост"
)
mock_deepseek_service.add_negative_example.assert_called_once_with(
"Отклоненный пост"
)