126 lines
5.6 KiB
Python
126 lines
5.6 KiB
Python
|
|
"""Цепочка 112 → ДДС: карточка уходит снимком, диспетчер подтверждает или отбивает."""
|
|||
|
|
|
|||
|
|
import time
|
|||
|
|
from uuid import uuid4
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from fastapi.testclient import TestClient
|
|||
|
|
|
|||
|
|
from app.main import app
|
|||
|
|
from app.session.hub import hub
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def client():
|
|||
|
|
with TestClient(app) as test_client:
|
|||
|
|
hub.journal = None
|
|||
|
|
yield test_client
|
|||
|
|
|
|||
|
|
|
|||
|
|
def wait_for(predicate, timeout: float = 3.0):
|
|||
|
|
deadline = time.monotonic() + timeout
|
|||
|
|
while time.monotonic() < deadline:
|
|||
|
|
value = predicate()
|
|||
|
|
if value:
|
|||
|
|
return value
|
|||
|
|
time.sleep(0.02)
|
|||
|
|
raise AssertionError("не дождались")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def read_until(ws, event_type: str, limit: int = 20) -> dict:
|
|||
|
|
for _ in range(limit):
|
|||
|
|
message = ws.receive_json()
|
|||
|
|
if message["type"] == event_type:
|
|||
|
|
return message
|
|||
|
|
raise AssertionError(f"событие {event_type} не пришло")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def lesson(client):
|
|||
|
|
session_id = uuid4()
|
|||
|
|
control = client.websocket_connect(f"/ws/control/{session_id}")
|
|||
|
|
socket = control.__enter__()
|
|||
|
|
socket.send_json({"type": "scenario.start", "scenario_id": "fire-apartment-l2",
|
|||
|
|
"trainee": "Иванов", "mode": "training"})
|
|||
|
|
wait_for(lambda: hub.get(session_id))
|
|||
|
|
return session_id, control, socket
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_card_goes_to_the_dispatcher_frozen(client):
|
|||
|
|
"""Снимок не меняется после передачи: оператор не дописывает задним числом."""
|
|||
|
|
session_id, control, _ = lesson(client)
|
|||
|
|
try:
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}?role=dds_01") as station:
|
|||
|
|
with client.websocket_connect(f"/ws/call/{session_id}") as trainee:
|
|||
|
|
trainee.send_json({"type": "call.answer"})
|
|||
|
|
trainee.send_json({"type": "kio.patch", "fields": {"address": "улица Ленина, 14", "dds": "01"}})
|
|||
|
|
wait_for(lambda: hub.get(session_id).kio.address)
|
|||
|
|
trainee.send_json({"type": "dds.dispatch", "service": "01"})
|
|||
|
|
received = read_until(station, "card.received")
|
|||
|
|
|
|||
|
|
# Правка после передачи в снимок не попадает.
|
|||
|
|
trainee.send_json({"type": "kio.patch", "fields": {"floor": "5"}})
|
|||
|
|
wait_for(lambda: hub.get(session_id).kio.floor == "5")
|
|||
|
|
|
|||
|
|
assert received["card"]["address"] == "улица Ленина, 14"
|
|||
|
|
assert received["card"]["floor"] is None, "снимок изменился после передачи"
|
|||
|
|
assert received["from_operator"] == "Иванов"
|
|||
|
|
finally:
|
|||
|
|
control.__exit__(None, None, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_station_joining_late_still_gets_the_card(client):
|
|||
|
|
"""Диспетчер садится за АРМ, когда вызов уже идёт."""
|
|||
|
|
session_id, control, _ = lesson(client)
|
|||
|
|
try:
|
|||
|
|
with client.websocket_connect(f"/ws/call/{session_id}") as trainee:
|
|||
|
|
trainee.send_json({"type": "call.answer"})
|
|||
|
|
trainee.send_json({"type": "dds.dispatch", "service": "01"})
|
|||
|
|
wait_for(lambda: hub.get(session_id).dispatched_card)
|
|||
|
|
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
|||
|
|
assert read_until(station, "card.received")["card"] is not None
|
|||
|
|
finally:
|
|||
|
|
control.__exit__(None, None, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_acknowledgement_stops_the_four_second_norm(client):
|
|||
|
|
from app.domain.timers import TimerCode
|
|||
|
|
|
|||
|
|
session_id, control, _ = lesson(client)
|
|||
|
|
try:
|
|||
|
|
state = hub.get(session_id)
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
|||
|
|
with client.websocket_connect(f"/ws/call/{session_id}") as trainee:
|
|||
|
|
trainee.send_json({"type": "call.answer"})
|
|||
|
|
trainee.send_json({"type": "dds.dispatch", "service": "01"})
|
|||
|
|
read_until(station, "card.received")
|
|||
|
|
station.send_json({"type": "card.ack"})
|
|||
|
|
measured = wait_for(lambda: state.timers.measured_ms(TimerCode.DDS_ACK) is not None)
|
|||
|
|
assert measured
|
|||
|
|
finally:
|
|||
|
|
control.__exit__(None, None, None)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_bounced_card_becomes_e6_with_the_reason(client):
|
|||
|
|
"""Неполнота КИО перестаёт быть процентом в отчёте и становится
|
|||
|
|
сорванным выездом с конкретной причиной."""
|
|||
|
|
session_id, control, _ = lesson(client)
|
|||
|
|
try:
|
|||
|
|
state = hub.get(session_id)
|
|||
|
|
with client.websocket_connect(f"/ws/station/{session_id}") as station:
|
|||
|
|
with client.websocket_connect(f"/ws/call/{session_id}") as trainee:
|
|||
|
|
trainee.send_json({"type": "call.answer"})
|
|||
|
|
trainee.send_json({"type": "dds.dispatch", "service": "01"})
|
|||
|
|
read_until(station, "card.received")
|
|||
|
|
station.send_json({"type": "card.bounce", "missing_fields": ["floor", "victims_count"],
|
|||
|
|
"comment": "куда ехать без этажа"})
|
|||
|
|
wait_for(lambda: state.bounced_fields)
|
|||
|
|
trainee.send_json({"type": "call.hangup"})
|
|||
|
|
wait_for(lambda: state.score is not None)
|
|||
|
|
|
|||
|
|
response = client.get(f"/api/sessions/{session_id}/report").json()
|
|||
|
|
chain = [finding for finding in response["findings"] if finding["code"] == "E6"]
|
|||
|
|
assert chain, "возврат карточки должен попасть в разбор"
|
|||
|
|
assert "floor" in chain[0]["summary"] and "выезд сорван" in chain[0]["summary"]
|
|||
|
|
finally:
|
|||
|
|
control.__exit__(None, None, None)
|