From 12329e7c5d87a491d64c411bc1f14a0425a26391 Mon Sep 17 00:00:00 2001 From: gglamer Date: Sat, 26 Sep 2026 21:12:00 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20card=5Fstatus=20=D0=B2=20=D0=BE=D1=82?= =?UTF-8?q?=D1=87=D1=91=D1=82=D0=B5=20=D0=94=D0=94=D0=A1=20=D0=B1=D0=B5?= =?UTF-8?q?=D1=80=D1=91=D1=82=D1=81=D1=8F=20=D1=83=20=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BA=D0=B8,=20=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B9=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=88=D0=B5=D0=BD=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/session/finish.py | 41 ++++++----- backend/tests/test_finish_card_status.py | 88 ++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 backend/tests/test_finish_card_status.py diff --git a/backend/app/session/finish.py b/backend/app/session/finish.py index da03aa5..c294078 100644 --- a/backend/app/session/finish.py +++ b/backend/app/session/finish.py @@ -150,6 +150,29 @@ def scoring_scenario(state): return state.scenario or store.get(state.scenario_id) +def score_live_dds_cards(state) -> list[DdsCardRecord]: + """Оценить все неоценённые карточки очереди и вернуть пульт курсанту. + + Оценка переключает рабочие поля сессии на каждую карточку по очереди. + Без возврата отчёт взял бы card_status у карточки, оценённой последней, + а пульт после завершения показывал бы чужую карточку. + """ + cards = list(state.dds_completed) + state.capture_active_dds() + active_id = state.dds_active_card_id + active_card = state.dispatched_card + for live in sorted(state.dds_live_cards, key=lambda item: item.original_index): + if any(item.card_id == live.card_id for item in cards): + continue + state.activate_dds_card(live.card_id, capture=False) + cards.append(score_current_dds(state)) + state.dds_completed = cards + if active_id is None or not state.activate_dds_card(active_id, capture=False): + state.dds_active_card_id = active_id + state.dispatched_card = active_card + return cards + + async def finish(session_id: UUID, state) -> None: if state.recorder is not None: path = await asyncio.to_thread(state.recorder.finalize) @@ -204,14 +227,7 @@ async def finish(session_id: UUID, state) -> None: # Все карточки выданы одновременно: при досрочном завершении оцениваем # каждую, включая не открытую, потому что её норматив уже шёл. result = GostResult() - cards = list(state.dds_completed) - state.capture_active_dds() - for live in sorted(state.dds_live_cards, key=lambda item: item.original_index): - if any(item.card_id == live.card_id for item in cards): - continue - state.activate_dds_card(live.card_id, capture=False) - cards.append(score_current_dds(state)) - state.dds_completed = cards + cards = score_live_dds_cards(state) for card in cards: result.metrics.extend(card.metrics) result.findings.extend(card.findings) @@ -283,14 +299,7 @@ async def finish(session_id: UUID, state) -> None: if state.handoff_to_dds and state.dds_scenarios: # В связке КИО оценивается относительно эталона и весов упражнения # 112, а каждая карточка очереди уже взвешена собственным сценарием. - cards = list(state.dds_completed) - state.capture_active_dds() - for live in sorted(state.dds_live_cards, key=lambda item: item.original_index): - if any(item.card_id == live.card_id for item in cards): - continue - state.activate_dds_card(live.card_id, capture=False) - cards.append(score_current_dds(state)) - state.dds_completed = cards + cards = score_live_dds_cards(state) for card in cards: result.metrics.extend(card.metrics) result.findings.extend(card.findings) diff --git a/backend/tests/test_finish_card_status.py b/backend/tests/test_finish_card_status.py new file mode 100644 index 0000000..fdf19ae --- /dev/null +++ b/backend/tests/test_finish_card_status.py @@ -0,0 +1,88 @@ +"""card_status отчёта — статус карточки, активной у курсанта при завершении.""" + +import asyncio +from pathlib import Path +from uuid import uuid4 + +import pytest +from app.domain.events import Exercise, SessionMode +from app.domain.kio import KIO +from app.domain.statuses import CardStatus, ServiceStatus +from app.scenarios.loader import load_file +from app.session import finish as finish_module +from app.session.dds import prepare_handoff_queue, prepare_queue +from app.session.hub import hub +from app.session.state import SessionState + +LIBRARY = Path(__file__).resolve().parents[2] / "scenarios" + + +@pytest.fixture(autouse=True) +def offline_finish(monkeypatch): + class NoCoaching: + def model_dump(self, **_kwargs): + return {} + + async def no_coach(_metrics): + return NoCoaching() + + monkeypatch.setattr(finish_module, "coach", no_coach) + monkeypatch.setattr(hub, "journal", None) + + +def accept_active_card(state: SessionState) -> None: + service = state.notified_services()[0] + state.set_service_status( + service, ServiceStatus.ACCEPTED, + "Основание: доклад старшего.\nСведения: карточка принята.", + ) + + +def assert_active_card_kept(state: SessionState, active_id, active_status) -> None: + assert active_status is not CardStatus.REGISTERED + assert state.score["card_status"] == active_status.value + assert state.dds_active_card_id == active_id + assert state.dispatched_card.card_id == active_id + assert len(state.score["card_results"]) == 2 + + +def test_dds_finish_reports_status_of_active_card_not_last_scored(): + first = load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY) + second = load_file(LIBRARY / "tickets" / "t20-2-stroke.yaml", LIBRARY) + state = SessionState( + session_id=uuid4(), scenario_id=first.id, scenario_title=first.title, + level=first.level.value, mode=SessionMode.TRAINING, exercise=Exercise.DDS, + ) + prepare_queue(state, [first, second]) + assert len(state.dds_live_cards) == 2 + active_id = state.dds_active_card_id + accept_active_card(state) + active_status = state.station_snapshot().card + + asyncio.run(finish_module.finish(state.session_id, state)) + + assert_active_card_kept(state, active_id, active_status) + + +def test_handoff_finish_reports_status_of_active_card_not_last_scored(): + first = load_file(LIBRARY / "fire-apartment-l2.yaml", LIBRARY) + second = load_file(LIBRARY / "tickets" / "t01-1-fire-container.yaml", LIBRARY) + state = SessionState( + session_id=uuid4(), scenario_id=first.id, scenario_title=first.title, + level=first.level.value, mode=SessionMode.TRAINING, + exercise=Exercise.CARD, handoff_to_dds=True, scenario=first, + pending_dds_scenarios=[second], + ) + state.kio = KIO( + address="улица Ленина, 14", description="горит балкон", notify=["Служба 101"], + ) + state.dispatch() + prepare_handoff_queue(state, state.pending_dds_scenarios) + assert len(state.dds_live_cards) == 2 + active_id = state.dds_active_card_id + accept_active_card(state) + active_status = state.station_snapshot().card + + asyncio.run(finish_module.finish(state.session_id, state)) + + assert_active_card_kept(state, active_id, active_status)