Complete incident selection and 112 to DDS workflow

This commit is contained in:
andreysk0304 2026-09-21 20:10:57 +03:00
commit 68dd83c7c2
21 changed files with 281 additions and 56 deletions

View file

@ -36,12 +36,13 @@ def read_until(socket, wanted):
raise AssertionError(f"событие {wanted} не пришло")
def start(client):
def start(client, *, handoff_to_dds=False):
session_id = uuid4()
context = client.websocket_connect(f"/ws/control/{session_id}")
control = context.__enter__()
control.send_json({"type": "scenario.start", "scenario_id": "fire-apartment-l2",
"trainee": "Иванов", "mode": "training", "exercise": "card"})
"trainee": "Иванов", "mode": "training", "exercise": "card",
"handoff_to_dds": handoff_to_dds})
wait_for(lambda: hub.get(session_id))
return session_id, context
@ -123,3 +124,53 @@ def test_incomplete_card_has_only_card_findings(client):
assert report["reference_questions"] == []
finally:
control.__exit__(None, None, None)
def test_filled_112_card_continues_as_dds_in_the_same_lesson(client):
session_id, control = start(client, handoff_to_dds=True)
try:
with client.websocket_connect(f"/ws/call/{session_id}") as trainee:
briefing = read_until(trainee, "card.briefing")
assert briefing["handoff_to_dds"] is True
trainee.send_json({"type": "kio.patch", "fields": {
"address": "улица Ленина, 14", "incident_type": "fire",
"victims_count": 2, "description": "горит балкон",
"signs": ["жилой дом", "балкон", "открытое пламя"],
}})
wait_for(lambda: hub.get(session_id).kio.incident_code)
trainee.send_json({"type": "card.submit"})
assert read_until(trainee, "call.ended")["reason"] == "complete"
state = hub.get(session_id)
assert state.dispatched_card is not None and not state.ended
assert state.score is None
trainee.send_json({"type": "kio.patch", "fields": {"address": "поздняя правка"}})
assert "передана" in read_until(trainee, "error")["message"]
with client.websocket_connect(f"/ws/call/{session_id}") as late:
assert read_until(late, "card.briefing")["card"]["address"] == "улица Ленина, 14"
assert read_until(late, "call.ended")["reason"] == "complete"
with client.websocket_connect(f"/ws/station/{session_id}") as station:
assert read_until(station, "card.received")["card"]["address"] == "улица Ленина, 14"
snapshot = read_until(station, "station.state")["snapshot"]
service = snapshot["services"][0]
crew = next(item for item in snapshot["crew_options"] if item.startswith(service + " — "))
station.send_json({"type": "card.status", "service": service, "status": "accepted"})
read_until(station, "station.state")
station.send_json({"type": "crew.select", "crew": crew})
read_until(station, "station.state")
station.send_json({"type": "phone.dial"})
assert read_until(station, "phone.report")["phase"] == "dispatched"
read_until(station, "station.state")
station.send_json({"type": "card.status", "service": service, "status": "responding"})
read_until(station, "station.state")
station.send_json({"type": "station.finish"})
read_until(station, "score.ready")
score = wait_for(lambda: hub.get(session_id).score)
assert hub.get(session_id).ended
keys = {item["key"] for item in score["metrics"]}
assert {"address", "incident_signs", "dds_primary", "dds_contact"} <= keys
assert score["score_auto"] < 100
finally:
control.__exit__(None, None, None)

View file

@ -5,9 +5,11 @@
"""
import pytest
from pathlib import Path
from app.domain import ekp
from app.domain.kio import KIO, derive_incident
from app.scenarios.loader import load_library
def test_reference_loads_whole_book():
@ -20,6 +22,20 @@ def test_reference_loads_whole_book():
assert all(incident.type for incident in reference.incidents), "код без итогового типа"
def test_all_customer_ticket_cards_are_complete_and_classified():
cards = [scenario for scenario in load_library(Path(__file__).resolve().parents[2] / "scenarios")
if scenario.ticket is not None]
assert len(cards) == 96
assert {scenario.ticket for scenario in cards} == set(range(1, 33))
for ticket in range(1, 33):
assert {scenario.position for scenario in cards if scenario.ticket == ticket} == {1, 2, 3}
assert all(scenario.facts and scenario.signs and scenario.ground_truth.address
and scenario.ground_truth.victims is not None
and scenario.ground_truth.incident_code
and ekp.incident(scenario.ground_truth.incident_code)
for scenario in cards)
def test_signs_give_the_code_from_the_book():
found = ekp.by_signs(["жилой дом", "подъезд", "открытое пламя"])
assert found is not None
@ -40,6 +56,20 @@ def test_next_level_narrows_by_previous():
assert "мусор" not in second, "«мусор» относится к улице, а не к жилому дому"
def test_category_filters_all_three_incident_choices_and_card_derivation():
fire = ekp.by_signs(["жилой дом", "балкон", "открытое пламя"])
assert fire is not None
other_group = next(group.id for group in ekp.groups() if group.id != fire.group)
assert ekp.by_signs(fire.signs, fire.group) == fire
assert ekp.by_signs(fire.signs, other_group) is None
assert fire.signs[0] in ekp.signs_at_level(1, group=fire.group)
assert fire.signs[1] in ekp.signs_at_level(2, fire.signs[:1], group=fire.group)
assert fire.signs[2] in ekp.signs_at_level(3, fire.signs[:2], group=fire.group)
card = derive_incident(KIO(incident_group=fire.group, signs=fire.signs))
assert card.incident_code == fire.code
assert derive_incident(KIO(incident_group=other_group, signs=fire.signs)).incident_code is None
def test_service_rows_hidden_from_operator_are_not_offered():
"""Часть строк книги существует ради интеграции и оператору не показывается."""
assert all(ekp.HIDDEN_SIGN not in sign.lower() for sign in ekp.signs_at_level(1))