From 83b7f8ffdd4d4d5cdd83e9fe7f4924cf7d286469 Mon Sep 17 00:00:00 2001 From: gglamer Date: Sat, 26 Sep 2026 19:53:45 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20timer.tick=20=D0=B4=D0=BE=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=20=D0=B4=D0=BE=20=D0=BF=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D0=B0=20=D0=94=D0=94=D0=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/ws/call.py | 4 ++- backend/app/session/hub.py | 6 +++- backend/tests/test_station_ticker.py | 44 ++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 backend/tests/test_station_ticker.py diff --git a/backend/app/api/ws/call.py b/backend/app/api/ws/call.py index 82471d4..121a28b 100644 --- a/backend/app/api/ws/call.py +++ b/backend/app/api/ws/call.py @@ -337,7 +337,9 @@ async def _handle(session_id: UUID, state, event) -> None: # первого своего действия: отмечаться ему по нему же (lct-33). hub.to_station(session_id, StationState(snapshot=state.station_snapshot())) hub.to_observers(session_id, KioState(kio=state.kio)) - hub.to_observers(session_id, TimerTick(timers=state.timers.snapshot())) + tick = TimerTick(timers=state.timers.snapshot()) + hub.to_observers(session_id, tick) + hub.to_station(session_id, tick) case "call.resolve": # Курсант закрывает вызов не карточкой. Отдельное действие, а не diff --git a/backend/app/session/hub.py b/backend/app/session/hub.py index ef1944f..bb20c0a 100644 --- a/backend/app/session/hub.py +++ b/backend/app/session/hub.py @@ -328,7 +328,11 @@ class SessionHub: # Keep the pending count and countdown live even while # the active dispatcher card is being handled. self.to_station(session_id, StationState(snapshot=state.station_snapshot())) - self.broadcast(session_id, TimerTick(timers=state.timers.snapshot())) + tick = TimerTick(timers=state.timers.snapshot()) + self.broadcast(session_id, tick) + # Срок первичного решения пульт берёт отсюда: после передачи + # из 112 без очереди ДДС лимит преподавателя больше нигде не виден. + self.to_station(session_id, tick) except asyncio.CancelledError: raise diff --git a/backend/tests/test_station_ticker.py b/backend/tests/test_station_ticker.py new file mode 100644 index 0000000..23ffb63 --- /dev/null +++ b/backend/tests/test_station_ticker.py @@ -0,0 +1,44 @@ +"""Такт таймеров доходит до АРМ ДДС: пульт строит срок решения по снимку сервера.""" + +import asyncio +from pathlib import Path +from uuid import uuid4 + +from app.domain.events import Exercise, SessionMode, TimerTick +from app.domain.timers import TimerCode +from app.scenarios.loader import load_file +from app.session import hub as hub_module +from app.session.hub import SessionHub +from app.session.state import SessionState + +LIBRARY = Path(__file__).resolve().parents[2] / "scenarios" + + +async def test_station_receives_dds_ack_with_configured_limit_after_handoff(monkeypatch): + """Передача из 112 без очереди ДДС: `queue_cards` пуст, и лимит решения, + настроенный преподавателем, пульт видит только в `timer.tick`.""" + monkeypatch.setattr(hub_module, "TICK_SECONDS", 0.01) + scenario = load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY) + state = SessionState( + session_id=uuid4(), scenario_id=scenario.id, scenario_title=scenario.title, + level=scenario.level.value, mode=SessionMode.TRAINING, + exercise=Exercise.CARD, handoff_to_dds=True, scenario=scenario, + ) + state.timers.limits[TimerCode.DDS_ACK] = 45_000 + state.on_event("dds.dispatch") + state.dispatch() + local_hub = SessionHub() + local_hub.register(state) + + with local_hub.station(state.session_id) as station: + local_hub.start_ticker(state.session_id) + try: + async with asyncio.timeout(1): + while not isinstance(event := await station.get(), TimerTick): + pass + finally: + local_hub.stop_ticker(state.session_id) + + ack = next(timer for timer in event.timers if timer.code is TimerCode.DDS_ACK) + assert ack.limit_ms == 45_000 + assert not ack.stopped