Словесное пояснение приоритетных направлений (дополнение к матрасчёту)
- rules_analysis._build_zone_reason: детерминированный текст по тем же факторам, что считает WeightedScorer — рельеф (лесистость %), вода (расстояние + акцент берегов при не_умеет_плавать), дороги (плотность + вероятное следование велосипеда), НП (ориентир), совпадение с направлением последнего свидетеля (точное/соседний румб), полосы психотипа. Финал — балл оценки. - ФИКС: weight 'direction' (0.07) был мёртвым — direction_match никогда не передавался в score_zone (всегда дефолт 0.5); теперь 1.0 при совпадении с last_seen_direction. - Фронт уже отображает reasoning на карточках зон — бэкенд-фикс достаточен. - Тесты: 7 новых на пояснения (детерминированность, факторы, фолбэк). 238 passed.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
"""Тесты словесных пояснений приоритетных направлений (_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
|
||||
Reference in New Issue
Block a user