""" Тесты для сервисов 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( "Отклоненный пост" )