""" Tests for claude_service.py Tests the Claude AI analysis with fallback to scoring service. """ import pytest from unittest.mock import AsyncMock, patch, MagicMock import os from services.claude_service import ( analyze_case, analyze_with_fallback, AnalysisResult, PrimaryZone ) class TestAnalysisResult: """Test AnalysisResult model.""" def test_analysis_result_structure(self): """Test AnalysisResult has correct structure.""" result = AnalysisResult( urgency="высокая", primary_zones=[ PrimaryZone( priority=1, name="Зона А", direction="N", distance=1.0, reason="Тест" ) ], search_radius_km=5.0, key_locations=["водоёмы"], behavioral_prediction="Тест", immediate_actions=["Действие 1"], summary="Тест", fallback_used=False ) assert result.urgency == "высокая" assert len(result.primary_zones) == 1 assert result.search_radius_km == 5.0 assert result.fallback_used is False class TestFallbackAnalysis: """Test fallback analysis without Claude API.""" @pytest.mark.asyncio async def test_fallback_young_child(self): """Test fallback for young child (critical urgency).""" case_data = { 'age': 3, 'gender': 'мужской', 'terrain': 'лес', 'weather': 'ясно' } result = await analyze_with_fallback(case_data) assert result.urgency == "критическая" assert result.fallback_used is True assert len(result.primary_zones) >= 2 assert "водоёмы" in result.key_locations @pytest.mark.asyncio async def test_fallback_with_ras_profile(self): """Test fallback with РАС profile.""" case_data = { 'age': 8, 'gender': 'мужской', 'profiles': ['РАС'] } result = await analyze_with_fallback(case_data) assert result.urgency == "критическая" assert "РАС" in result.behavioral_prediction or "водоём" in result.behavioral_prediction assert any("водоём" in action.lower() for action in result.immediate_actions) @pytest.mark.asyncio async def test_fallback_with_bicycle(self): """Test fallback with bicycle profile.""" case_data = { 'age': 12, 'gender': 'мужской', 'profiles': ['велосипед'] } result = await analyze_with_fallback(case_data) assert result.urgency == "высокая" assert any("10-15 км" in action or "камер" in action for action in result.immediate_actions) @pytest.mark.asyncio async def test_fallback_teenager(self): """Test fallback for teenager.""" case_data = { 'age': 15, 'gender': 'мужской', 'terrain': 'лес' } result = await analyze_with_fallback(case_data) assert result.urgency in ["средняя", "высокая"] assert "дороги" in result.key_locations or "населённые пункты" in result.key_locations @pytest.mark.asyncio async def test_fallback_with_coordinates(self): """Test fallback with coordinates (geo service integration).""" case_data = { 'age': 10, 'lat': 53.9, 'lon': 27.5, 'terrain': 'лес' } with patch('services.claude_service.build_search_zones', new_callable=AsyncMock) as mock_zones: # Mock zones from services.geo_service import Zone mock_zones.return_value = [ Zone( direction="N", distance_km=0.5, forest_pct=60.0, road_density=1.0, water_distance_km=2.0, settlement_distance_km=5.0 ), Zone( direction="E", distance_km=1.0, forest_pct=40.0, road_density=2.0, water_distance_km=1.0, settlement_distance_km=3.0 ) ] result = await analyze_with_fallback(case_data) assert result.fallback_used is True assert len(result.primary_zones) > 0 mock_zones.assert_called_once() @pytest.mark.asyncio async def test_fallback_without_coordinates(self): """Test fallback without coordinates (basic zones).""" case_data = { 'age': 10, 'terrain': 'лес' } result = await analyze_with_fallback(case_data) assert result.fallback_used is True assert len(result.primary_zones) >= 2 assert result.primary_zones[0].priority == 1 class TestAnalyzeCase: """Test main analyze_case function.""" @pytest.mark.asyncio async def test_analyze_without_api_key(self): """Test analyze falls back when no API key.""" case_data = { 'age': 10, 'gender': 'мужской', 'terrain': 'лес' } with patch.dict(os.environ, {'ANTHROPIC_API_KEY': ''}, clear=True): result = await analyze_case(case_data) assert result.fallback_used is True assert isinstance(result, AnalysisResult) @pytest.mark.asyncio async def test_analyze_with_api_error(self): """Test analyze falls back on API error.""" case_data = { 'age': 10, 'gender': 'мужской', 'terrain': 'лес' } with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test_key'}): with patch('httpx.AsyncClient') as mock_client: mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "Server error" mock_client.return_value.__aenter__.return_value.post = AsyncMock(return_value=mock_response) result = await analyze_case(case_data) # Should fallback assert result.fallback_used is True @pytest.mark.asyncio async def test_analyze_with_successful_api(self): """Test analyze with successful Claude API response.""" case_data = { 'age': 10, 'gender': 'мужской', 'terrain': 'лес', 'weather': 'дождь' } mock_api_response = { "urgency": "высокая", "primary_zones": [ { "priority": 1, "name": "Лесной массив север", "direction": "N", "distance": 1.5, "reason": "Наиболее вероятное направление" } ], "search_radius_km": 5.0, "key_locations": ["водоёмы", "дороги"], "behavioral_prediction": "Ребёнок может двигаться по тропам", "immediate_actions": ["Организовать поиск", "Проверить водоёмы"], "summary": "Случай высокой срочности" } with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test_key'}): with patch('httpx.AsyncClient') as mock_client: mock_response = MagicMock() mock_response.status_code = 200 import json json_text = json.dumps(mock_api_response, ensure_ascii=False) mock_response.json.return_value = { "content": [ { "text": f"```json\n{json_text}\n```" } ] } mock_client.return_value.__aenter__.return_value.post = AsyncMock(return_value=mock_response) result = await analyze_case(case_data) assert result.fallback_used is False assert result.urgency == "высокая" class TestUrgencyClassification: """Test urgency classification logic.""" @pytest.mark.asyncio async def test_critical_urgency_young_child(self): """Test critical urgency for very young children.""" case_data = {'age': 2} result = await analyze_with_fallback(case_data) assert result.urgency == "критическая" @pytest.mark.asyncio async def test_critical_urgency_epilepsy(self): """Test critical urgency for epilepsy.""" case_data = {'age': 10, 'profiles': ['эпилепсия']} result = await analyze_with_fallback(case_data) assert result.urgency == "критическая" @pytest.mark.asyncio async def test_high_urgency_bicycle(self): """Test high urgency for bicycle.""" case_data = {'age': 12, 'profiles': ['велосипед']} result = await analyze_with_fallback(case_data) assert result.urgency == "высокая" @pytest.mark.asyncio async def test_medium_urgency_preteen(self): """Test medium urgency for preteen.""" case_data = {'age': 10} result = await analyze_with_fallback(case_data) assert result.urgency == "средняя" class TestKeyLocations: """Test key locations based on age.""" @pytest.mark.asyncio async def test_young_child_locations(self): """Test key locations for young children.""" case_data = {'age': 5} result = await analyze_with_fallback(case_data) assert "водоёмы" in result.key_locations assert "укрытия" in result.key_locations @pytest.mark.asyncio async def test_preteen_locations(self): """Test key locations for preteens.""" case_data = {'age': 10} result = await analyze_with_fallback(case_data) assert "водоёмы" in result.key_locations assert "дороги" in result.key_locations or "тропы" in result.key_locations @pytest.mark.asyncio async def test_teenager_locations(self): """Test key locations for teenagers.""" case_data = {'age': 15} result = await analyze_with_fallback(case_data) assert "дороги" in result.key_locations or "населённые пункты" in result.key_locations class TestBehavioralPrediction: """Test behavioral prediction logic.""" @pytest.mark.asyncio async def test_ras_prediction(self): """Test РАС behavioral prediction.""" case_data = {'age': 8, 'profiles': ['РАС']} result = await analyze_with_fallback(case_data) assert "водоём" in result.behavioral_prediction.lower() or "рас" in result.behavioral_prediction.lower() @pytest.mark.asyncio async def test_young_child_prediction(self): """Test young child behavioral prediction.""" case_data = {'age': 3} result = await analyze_with_fallback(case_data) assert "минимальное" in result.behavioral_prediction.lower() or "близко" in result.behavioral_prediction.lower() @pytest.mark.asyncio async def test_teenager_prediction(self): """Test teenager behavioral prediction.""" case_data = {'age': 15} result = await analyze_with_fallback(case_data) assert "целенаправленное" in result.behavioral_prediction.lower() or "населённ" in result.behavioral_prediction.lower() class TestImmediateActions: """Test immediate actions generation.""" @pytest.mark.asyncio async def test_basic_actions(self): """Test basic immediate actions are present.""" case_data = {'age': 10} result = await analyze_with_fallback(case_data) assert len(result.immediate_actions) >= 3 assert any("поиск" in action.lower() for action in result.immediate_actions) @pytest.mark.asyncio async def test_ras_critical_action(self): """Test РАС critical action is first.""" case_data = {'age': 8, 'profiles': ['РАС']} result = await analyze_with_fallback(case_data) first_action = result.immediate_actions[0] assert "водоём" in first_action.lower() or "критично" in first_action.lower() @pytest.mark.asyncio async def test_bicycle_action(self): """Test bicycle specific action.""" case_data = {'age': 12, 'profiles': ['велосипед']} result = await analyze_with_fallback(case_data) assert any("10-15" in action or "камер" in action for action in result.immediate_actions)