style: isort + black
This commit is contained in:
@@ -6,16 +6,19 @@ 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)
|
||||
from helper_bot.services.scoring.exceptions import (
|
||||
InsufficientExamplesError,
|
||||
ScoringError,
|
||||
TextTooShortError,
|
||||
)
|
||||
|
||||
|
||||
class TestScoringResult:
|
||||
"""Тесты для ScoringResult."""
|
||||
|
||||
|
||||
def test_create_valid_score(self):
|
||||
"""Тест создания валидного результата."""
|
||||
result = ScoringResult(
|
||||
@@ -26,17 +29,17 @@ class TestScoringResult:
|
||||
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(
|
||||
@@ -47,12 +50,12 @@ class TestScoringResult:
|
||||
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 = {
|
||||
@@ -62,7 +65,7 @@ class TestScoringResult:
|
||||
"confidence": 0.9,
|
||||
}
|
||||
result = ScoringResult.from_dict("rag", data)
|
||||
|
||||
|
||||
assert result.score == 0.75
|
||||
assert result.source == "rag"
|
||||
assert result.model == "test-model"
|
||||
@@ -72,49 +75,55 @@ class TestScoringResult:
|
||||
|
||||
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")
|
||||
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)
|
||||
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
|
||||
@@ -122,38 +131,40 @@ class TestCombinedScore:
|
||||
|
||||
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
|
||||
@@ -162,18 +173,18 @@ class TestVectorStore:
|
||||
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
|
||||
@@ -182,86 +193,86 @@ class TestVectorStore:
|
||||
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
|
||||
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
|
||||
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
|
||||
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):
|
||||
"""Тест расчета скора для короткого текста."""
|
||||
@@ -271,125 +282,143 @@ class TestDeepSeekService:
|
||||
|
||||
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.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.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):
|
||||
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):
|
||||
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"))
|
||||
|
||||
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("Опубликованный пост")
|
||||
|
||||
|
||||
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("Отклоненный пост")
|
||||
|
||||
mock_rag_service.add_negative_example.assert_called_once_with(
|
||||
"Отклоненный пост"
|
||||
)
|
||||
mock_deepseek_service.add_negative_example.assert_called_once_with(
|
||||
"Отклоненный пост"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user