"""Тесты детерминированного rules-анализа (services/rules_analysis.py). Ключевые кейсы перенесены из test_claude_service.py (удалён вместе с LLM); плюс проверка отсутствия внешних вызовов. """ from __future__ import annotations from services.rules_analysis import ( AnalysisResult, PrimaryZone, rule_based_urgency, rules_analyze, ) class TestUrgency: async def test_young_child_critical(self): # _age_urgency: возраст ≤4 — критическая вне зависимости от elapsed r = await rules_analyze({'age': 3}) assert r.urgency == 'критическая' def test_ras_critical(self): assert rule_based_urgency(1.0, 8, ['РАС']) == 'высокая' # возраст 8 не даёт +2 def test_long_search_raises(self): assert rule_based_urgency(30.0, 15, []) == 'высокая' assert rule_based_urgency(30.0, 5, ['РАС']) == 'критическая' class TestFallbackRules: async def test_young_child(self): result = await rules_analyze({'age': 3, 'gender': 'мужской', 'terrain': 'лес'}) assert isinstance(result, AnalysisResult) assert result.urgency == 'критическая' assert result.fallback_used is True assert len(result.primary_zones) >= 2 assert 'водоёмы' in result.key_locations async def test_ras_profile(self): result = await rules_analyze({'age': 8, 'profiles': ['РАС']}) assert result.urgency == 'критическая' assert 'РАС' in result.behavioral_prediction or 'водоём' in result.behavioral_prediction assert any('водоём' in a.lower() for a in result.immediate_actions) async def test_bicycle_profile(self): result = await rules_analyze({'age': 12, 'profiles': ['велосипед']}) assert result.urgency == 'высокая' assert any('10-15 км' in a or 'камер' in a for a in result.immediate_actions) async def test_teenager(self): result = await rules_analyze({'age': 15, 'terrain': 'лес'}) assert result.urgency in ['средняя', 'высокая'] assert 'дороги' in result.key_locations or 'населённые пункты' in result.key_locations async def test_behavioral_predictions_by_age(self): r4 = await rules_analyze({'age': 4, 'profiles': []}) r9 = await rules_analyze({'age': 9, 'profiles': []}) r16 = await rules_analyze({'age': 16, 'profiles': []}) assert 'близко к точке потери' in r4.behavioral_prediction assert 'тропам или дорогам' in r9.behavioral_prediction assert 'населённым пунктам' in r16.behavioral_prediction class TestNoLLM: def test_no_anthropic_in_services(self): """В services/ не осталось ссылок на LLM-провайдеров.""" import pathlib for p in pathlib.Path('services').glob('*.py'): src = p.read_text(encoding='utf-8') for word in ('anthropic', 'ANTHROPIC', 'api.anthropic.com', 'openai', 'OpenAI'): assert word not in src, f'{p.name} contains {word}' def test_no_llm_keys_in_compose(self): src = open('docker-compose.yml', encoding='utf-8').read() assert 'ANTHROPIC' not in src and 'OPENAI' not in src async def test_result_is_deterministic(self): a = await rules_analyze({'age': 10, 'profiles': []}) b = await rules_analyze({'age': 10, 'profiles': []}) assert a.model_dump() == b.model_dump()