136 lines
5.1 KiB
Python
136 lines
5.1 KiB
Python
|
|
"""Исход вызова: не каждый звонок заканчивается карточкой.
|
|||
|
|
|
|||
|
|
Пункты приёмки lct-36. Материал — билет 1 заказчика: третий вызов в нём
|
|||
|
|
из Волгоградской области, и правильное действие — передать, а не заводить
|
|||
|
|
карточку (docs/spec/TICKETS.md).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from app.domain.classifiers import Outcome
|
|||
|
|
from app.domain.kio import KIO
|
|||
|
|
from app.scenarios.loader import load_file, load_library
|
|||
|
|
from app.scoring.gost import evaluate
|
|||
|
|
from app.session.timers import SessionTimers
|
|||
|
|
|
|||
|
|
LIBRARY = Path(__file__).resolve().parents[2] / "scenarios"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="module")
|
|||
|
|
def other_region():
|
|||
|
|
return load_file(LIBRARY / "tickets" / "t01-3-child-other-region.yaml", LIBRARY)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture(scope="module")
|
|||
|
|
def ordinary():
|
|||
|
|
return load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def timeline() -> SessionTimers:
|
|||
|
|
timers = SessionTimers()
|
|||
|
|
timers.on_event("call.incoming", now=0.0)
|
|||
|
|
timers.on_event("call.answer", now=5.0)
|
|||
|
|
return timers
|
|||
|
|
|
|||
|
|
|
|||
|
|
def score(scenario, *, resolved=None, dispatched=False, kio=None):
|
|||
|
|
return evaluate(
|
|||
|
|
scenario=scenario,
|
|||
|
|
kio=kio or KIO(),
|
|||
|
|
timers=timeline(),
|
|||
|
|
revealed_facts=[fact.id for fact in scenario.facts],
|
|||
|
|
refined_facts=[fact.id for fact in scenario.facts if fact.refine_on],
|
|||
|
|
resolved_outcome=resolved,
|
|||
|
|
dispatched=dispatched,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_default_outcome_is_a_card(ordinary):
|
|||
|
|
assert ordinary.outcome is Outcome.CARD
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_transfer_is_declared_by_the_scenario(other_region):
|
|||
|
|
assert other_region.outcome is Outcome.TRANSFER_REGION
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_correct_transfer_passes(other_region):
|
|||
|
|
result = score(other_region, resolved="transfer_region")
|
|||
|
|
metric = next(m for m in result.metrics if m.key == "outcome")
|
|||
|
|
assert metric.passed
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_filing_a_card_instead_of_transferring_is_an_error(other_region):
|
|||
|
|
"""Курсант завёл карточку и отправил наряд в другой субъект — это ошибка,
|
|||
|
|
которую прежняя оценка не видела вовсе."""
|
|||
|
|
result = score(other_region, dispatched=True)
|
|||
|
|
metric = next(m for m in result.metrics if m.key == "outcome")
|
|||
|
|
assert not metric.passed
|
|||
|
|
assert any(finding.code.value == "E2" for finding in result.findings)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_doing_nothing_is_also_an_error(other_region):
|
|||
|
|
result = score(other_region)
|
|||
|
|
assert not next(m for m in result.metrics if m.key == "outcome").passed
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_empty_card_is_not_punished_when_no_card_is_expected(other_region):
|
|||
|
|
"""Штрафовать за пустые поля там, где карточка не заводится, значит учить
|
|||
|
|
заполнять лишнее."""
|
|||
|
|
result = score(other_region, resolved="transfer_region")
|
|||
|
|
keys = {metric.key for metric in result.metrics}
|
|||
|
|
assert "required_fields" not in keys
|
|||
|
|
assert "address" not in keys
|
|||
|
|
assert "victims_count" not in keys
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_ordinary_call_has_no_outcome_metric(ordinary):
|
|||
|
|
"""У обычного вызова исход не обсуждается — метрики просто нет,
|
|||
|
|
а не «пройдена»."""
|
|||
|
|
result = score(ordinary, dispatched=True)
|
|||
|
|
assert "outcome" not in {metric.key for metric in result.metrics}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_interview_still_counts_for_a_transfer(other_region):
|
|||
|
|
"""Передать вызов — не значит не опрашивать: адрес и состояние ребёнка
|
|||
|
|
другому региону всё равно нужны."""
|
|||
|
|
result = score(other_region, resolved="transfer_region")
|
|||
|
|
assert "checklist_completeness" in {metric.key for metric in result.metrics}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── билет как единица занятия ──
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_ticket_one_is_three_calls_of_different_services():
|
|||
|
|
"""Билет собран так, чтобы за одно занятие курсант прошёл разные службы."""
|
|||
|
|
ticket = [s for s in load_library(LIBRARY) if s.ticket == 1]
|
|||
|
|
assert len(ticket) == 3
|
|||
|
|
assert sorted(s.position for s in ticket) == [1, 2, 3]
|
|||
|
|
assert len({s.type for s in ticket}) == 3
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_ticket_and_position_go_together():
|
|||
|
|
from pydantic import ValidationError
|
|||
|
|
|
|||
|
|
from app.scenarios.schema import Scenario
|
|||
|
|
|
|||
|
|
with pytest.raises(ValidationError):
|
|||
|
|
Scenario.model_validate(
|
|||
|
|
{
|
|||
|
|
"id": "broken",
|
|||
|
|
"title": "Билет без номера вызова",
|
|||
|
|
"type": "fire",
|
|||
|
|
"level": "L1",
|
|||
|
|
"ticket": 4,
|
|||
|
|
"persona": {"base": "calm"},
|
|||
|
|
"first_line": "Алло",
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_mass_fight_notifies_both_police_and_medics():
|
|||
|
|
"""Тот случай, где выбор одной службы был бы неверен по построению."""
|
|||
|
|
scenario = load_file(LIBRARY / "tickets" / "t01-2-mass-fight.yaml", LIBRARY)
|
|||
|
|
assert "МВД" in scenario.ground_truth.notify
|
|||
|
|
assert "СМП" in scenario.ground_truth.notify
|