1513bad538
- search_engine: max_distance_km передаётся в analyzer (case_data). - rules_analysis: search_radius = max_distance × _psychotype_radius_multiplier (среднее по полосам 0-500/0.5-1.5/1.5-2.5/2.5+). Убран двойной учёт age/season: score_zone.distance_multiplier их уже содержит, а max_distance их тоже учёл — раньше radius = multiplier×2.0 (хардкод) давал 1.6 км при радиусе 2.4 км. - UI: подсказка у «×0.8» — коэффициент скоринга, не радиуса. - Тест: согласованность summary↔формула.
90 lines
4.4 KiB
Python
90 lines
4.4 KiB
Python
"""Тесты словесных пояснений приоритетных направлений (_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
|