"""Тесты словесных пояснений приоритетных направлений (_build_zone_reason).""" from __future__ import annotations import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from services.rules_analysis import ( # noqa: E402 _build_zone_reason, _direction_phrase, _psychotype_phrase, _water_phrase, ) class TestZoneReason: def test_reason_mentions_key_factors(self): zone = {'direction': 'NE', 'distance_km': 2.0, 'forest_pct': 0.68, 'road_density': 0.5, 'water_distance_km': 0.4, 'settlement_distance_km': 1.2, 'score': 78.4} case = {'profiles': ['не_умеет_плавать'], 'last_seen_direction': 'ne'} reason = _build_zone_reason(zone, case) assert 'лесистость умеренная (68%)' in reason assert '0.4 км — проверить берега' in reason assert 'не умеет плавать' in reason assert 'совпадает с направлением' in reason assert reason.endswith('Оценка 78/100.') def test_reason_open_field_with_bike(self): zone = {'direction': 'SE', 'distance_km': 1.0, 'forest_pct': 0.12, 'road_density': 2.6, 'water_distance_km': 6.0, 'settlement_distance_km': None, 'score': 41.0} reason = _build_zone_reason(zone, {'profiles': ['велосипед']}) assert 'преобладает открытое поле' in reason assert 'вода далеко' in reason assert 'велосипед' in reason def test_reason_direction_neighbor(self): zone = {'direction': 'NE', 'distance_km': 1.0, 'score': 60.0} case = {'last_seen_direction': 'N'} reason = _build_zone_reason(zone, case) assert 'близко к направлению' in reason def test_reason_without_factors_falls_back_to_score(self): reason = _build_zone_reason({'direction': 'N', 'score': 50.0, 'distance_km': 1.0}, {}) assert reason == 'Оценка 50/100 по весам модели' def test_water_phrase_no_swimmer_far_water_no_warning(self): # Не умеет плавать, но вода далеко — акцент не добавляется phrase = _water_phrase(8.0, ['не_умеет_плавать']) assert phrase is not None and 'не умеет плавать' not in phrase def test_psychotype_phrase_bands(self): mods = {'zone_0_500': 1.0, 'zone_500_1500': 1.25, 'zone_1500_2500': 1.0, 'zone_2500plus': 0.8} up = _psychotype_phrase({'psychotype_modifiers': mods}, 1.0) assert 'повышает' in up and '0.5-1.5 км' in up down = _psychotype_phrase({'psychotype_modifiers': mods}, 3.0) assert 'снижает' in down def test_reason_is_deterministic(self): zone = {'direction': 'E', 'distance_km': 1.8, 'forest_pct': 0.5, 'road_density': 1.0, 'water_distance_km': 2.0, 'settlement_distance_km': 3.0, 'score': 55.5} case = {'profiles': [], 'last_seen_direction': 'E'} r1 = _build_zone_reason(zone, case) r2 = _build_zone_reason(dict(zone), dict(case)) assert r1 == r2 assert 'совпадает с направлением' in r1 class TestRadiusConsistency: def test_summary_radius_matches_max_times_multiplier(self): """Радиус в summary = max_distance × психотип-множитель (согласован с хедером).""" import asyncio from services.rules_analysis import rules_analyze case = { 'age': 9, 'gender': 'м', 'season': 'лето', 'elapsed_hours': 4.0, 'profiles': ['гармоничный'], 'psychotype': 'harmonic', 'psychotype_modifiers': {'zone_0_500': 1.0, 'zone_500_1500': 1.0, 'zone_1500_2500': 1.1, 'zone_2500plus': 1.0}, 'max_distance_km': 2.4, 'lat': 54.5, 'lon': 28.5, } result = asyncio.run(rules_analyze(case)) # Средний психотип-множитель полос (1.0+1.0+1.1+1.0)/4 = 1.025 → 2.4×1.025=2.46→2.5 assert result.search_radius_km == 2.5, result.search_radius_km assert f'{result.search_radius_km} км' in result.summary