3e38aae2be
Бэкенд:
- schemas.py: ClosedCaseCreate/Response — форма ~25 полей (субъект, среда,
ресурсы, исход) для внесения завершённых поисков вручную
- routers/closed_cases.py: CRUD + POST {id}/reanalyze. _run_model() —
тот же скоринг, что /analyze (WeightedScorer + distance_service +
психотип), НО БЕЗ claude_service: urgency/behavior/actions по правилам
- _accuracy_note(): прогнозный радиус vs факт (в радиусе / мимо)
- main.py: роутер подключён
Фронтенд:
- pages/ClosedCases.jsx + .css: форма внесения (VCL-density, чипы диагнозов/
рельефа), блок 'Прогноз vs факт', таблица архива (в радиусе/мимо)
- AdminDashboard.jsx: вкладки Кейсы | Завершённые поиски
Тесты: test_closed_cases.py — 6 passed. Существующие 15 failed
(test_distance_service) не связаны с изменениями — падали до них.
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Тесты завершённых поисков (ручной ввод, без ИИ)."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from backend.routers.closed_cases import (
|
|
_accuracy_note,
|
|
_rule_based_actions,
|
|
_rule_based_behavior,
|
|
_rule_based_urgency,
|
|
_run_model,
|
|
)
|
|
|
|
|
|
def test_rule_based_urgency_levels():
|
|
# Младенец, сутки поиска, диагноз → критическая
|
|
assert _rule_based_urgency(30, 3, ['РАС']) == 'критическая'
|
|
# Подросток, 2 часа, без диагнозов → средняя
|
|
assert _rule_based_urgency(2, 15, []) == 'средняя'
|
|
# 10 часов, 6 лет → высокая
|
|
assert _rule_based_urgency(10, 6, []) == 'высокая'
|
|
|
|
|
|
def test_rule_based_behavior_profiles():
|
|
data = {'has_transport': 'bike', 'cant_swim': True}
|
|
text = _rule_based_behavior(data, ['РАС'], None)
|
|
assert 'линейных ориентиров' in text
|
|
assert 'велик' in text
|
|
assert 'плавать' in text
|
|
|
|
|
|
def test_rule_based_actions_ras():
|
|
actions = _rule_based_actions({'elapsed_hours': 10, 'cant_swim': False}, ['РАС'], None)
|
|
assert any('кинолог' in a for a in actions)
|
|
assert any('укрыти' in a for a in actions)
|
|
|
|
|
|
def test_accuracy_note_inside_radius():
|
|
pred = {'max_distance_km': 5.4}
|
|
fact = type('F', (), {'found_distance_km': 2.0, 'found_alive': True})()
|
|
note = _accuracy_note(pred, fact)
|
|
assert 'внутри прогнозного радиуса' in note
|
|
assert '5.4' in note
|
|
|
|
|
|
def test_accuracy_note_outside_radius():
|
|
pred = {'max_distance_km': 3.0}
|
|
fact = type('F', (), {'found_distance_km': 7.0, 'found_alive': False})()
|
|
note = _accuracy_note(pred, fact)
|
|
assert 'ЗА пределами' in note
|
|
assert 'неблагоприятный' in note
|
|
|
|
|
|
def test_run_model_no_llm():
|
|
"""Ключевой контракт: прогон модели работает БЕЗ вызова LLM."""
|
|
data = {
|
|
'age_years': 8, 'gender': 'м', 'diagnosis_type': [],
|
|
'has_transport': 'none', 'cant_swim': False,
|
|
'elapsed_hours': 3, 'terrain': ['лес'], 'terrain_primary': 'лес',
|
|
'weather': 'нет', 'season': 'лето',
|
|
}
|
|
result = _run_model(data)
|
|
assert result['llm_used'] is False
|
|
assert result['fallback_used'] is False
|
|
assert result['max_distance_km'] > 0
|
|
assert 'coefficients' in result
|
|
assert result['coefficients']['base_speed'] > 0
|
|
# Для 8 лет, 3 часа, лес, день — радиус порядка нескольких км
|
|
assert 1 <= result['max_distance_km'] <= 15 |