Files
vector/backend/tests/test_closed_cases.py
Sadmin 3e38aae2be feat: завершённые поиски — ручной ввод + детерминированный прогон модели (без ИИ)
Бэкенд:
- 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) не связаны с изменениями — падали до них.
2026-09-09 10:41:33 +03:00

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