From 13642ec9734b89f87ab657422a799efb7b71241f Mon Sep 17 00:00:00 2001 From: Ivan Gerasimov Date: Sun, 27 Sep 2026 23:48:03 +0300 Subject: [PATCH] =?UTF-8?q?lct-52:=20=D0=B4=D0=B2=D0=B0=20=D1=81=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D1=8F=20=D1=81=20=D1=8D=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=BD=D0=BD=D1=8B=D0=BC=20=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=BE=D0=BC=20=D0=94=D0=94=D0=A1=20=E2=80=94=20?= =?UTF-8?q?=D0=B4=D1=83=D0=B1=D0=BB=D1=8C=20=D0=B8=20=D1=87=D1=83=D0=B6?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B7=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/test_decline_scenarios.py | 90 +++++++++++++++++++++++++ scenarios/dds-decline-duplicate.yaml | 54 +++++++++++++++ scenarios/dds-decline-out-of-zone.yaml | 55 +++++++++++++++ 3 files changed, 199 insertions(+) create mode 100644 backend/tests/test_decline_scenarios.py create mode 100644 scenarios/dds-decline-duplicate.yaml create mode 100644 scenarios/dds-decline-out-of-zone.yaml diff --git a/backend/tests/test_decline_scenarios.py b/backend/tests/test_decline_scenarios.py new file mode 100644 index 0000000..f97c574 --- /dev/null +++ b/backend/tests/test_decline_scenarios.py @@ -0,0 +1,90 @@ +"""Сценарии с эталонным отказом ДДС (lct-52). + +Мотивированный отказ по эталону не даёт отметок; «Принята» вопреки эталону — +D2 с причиной из сценария в норме. Причины — из памятки АРМ-112: не наша +зона, не наша компетенция, дубль. +""" + +from pathlib import Path +from uuid import uuid4 + +import pytest + +from app.domain.events import Exercise, SessionMode +from app.scenarios.loader import load_file +from app.session import finish as finish_module +from app.session.dds import prepare_queue +from app.session.hub import hub +from app.session.state import SessionState +from app.session.store import MemorySessionStore + +from tests.test_dds_desk_scores import station + +LIBRARY = Path(__file__).resolve().parents[2] / "scenarios" +DECLINES = ["dds-decline-duplicate.yaml", "dds-decline-out-of-zone.yaml"] + + +@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, "store", MemorySessionStore()) + + +def lesson(name: str) -> SessionState: + scenario = load_file(LIBRARY / name, LIBRARY) + state = SessionState( + session_id=uuid4(), scenario_id=scenario.id, scenario_title=scenario.title, + level=scenario.level.value, mode=SessionMode.TRAINING, exercise=Exercise.DDS, + ) + prepare_queue(state, [scenario]) + station(state, type="card.open", card_id=str(state.station_snapshot().card_id)) + return state + + +@pytest.mark.parametrize("name", DECLINES) +def test_library_scenario_expects_a_reasoned_decline(name): + scenario = load_file(LIBRARY / name, LIBRARY) + + assert scenario.dds_decision.expected == "decline" + assert scenario.dds_decision.reason.split(":")[0] in {"дубль", "не наша зона"} + + +@pytest.mark.parametrize("name", DECLINES) +def test_reasoned_decline_by_the_reference_leaves_a_clean_report(name): + state = lesson(name) + service = state.station_snapshot().managed_service + reason = load_file(LIBRARY / name, LIBRARY).dds_decision.reason + + station(state, type="card.status", service=service, status="declined", + comment=f"Основание: {reason}.\nСведения: причина отказа передана дежурному 112.") + station(state, type="station.finish") + + score = state.score + assert score["findings"] == [] + decision = next(item for item in score["metrics"] if item["key"] == "dds_decision") + assert decision["passed"] and decision["norm"] == reason + assert score["score_auto"] == 100.0 + + +@pytest.mark.parametrize("name", DECLINES) +def test_accepting_against_the_reference_is_d2_with_the_scenario_reason(name): + state = lesson(name) + reason = load_file(LIBRARY / name, LIBRARY).dds_decision.reason + + station(state, type="card.ack", + comment="Основание: карточка поступила.\nСведения: принята, доклад дежурному.") + station(state, type="station.finish") + + score = state.score + d2 = [item for item in score["findings"] + if item["code"] == "D2" and "вопреки эталону" in item["summary"]] + assert len(d2) == 1 and d2[0]["norm"] == reason + decision = next(item for item in score["metrics"] if item["key"] == "dds_decision") + assert not decision["passed"] diff --git a/scenarios/dds-decline-duplicate.yaml b/scenarios/dds-decline-duplicate.yaml new file mode 100644 index 0000000..deac70d --- /dev/null +++ b/scenarios/dds-decline-duplicate.yaml @@ -0,0 +1,54 @@ +# Эталонный отказ ДДС: дубль. Памятка АРМ-112 называет три причины «Не принята» — +# не наша зона, не наша компетенция, дубль (docs/spec/DATASET.md). Здесь карточка +# повторяет уже принятый вызов: реагирование идёт, вторая бригада не нужна. +# Мотивированный отказ — без отметок; «Принята» вопреки эталону — D2. +id: dds-decline-duplicate +title: "Повторный вызов: пожар в подъезде уже в работе" +type: fire +level: L2 +topics: [fires] +modes: [training, exam, self] + +extends: checklists/fire.yaml +signs: [жилой дом, подъезд, открытое пламя] + +persona: + base: worried + arc: + - { stage: registration, mood: worried } + - { stage: interview, mood: worried } + +first_line: "Алло, я уже звонил. В подъезде горит, пожарные подъехали, но дыма всё больше." + +facts: + - id: f_what_burns + value: "повторный вызов по уже зарегистрированному пожару в подъезде; пожарная бригада на месте, заявитель видит машины у дома" + reveal_on: { question: q_what_burns } + - id: f_address + value: "Москва, улица Академика Королёва, 15, подъезд 2" + reveal_on: { question: q_address } + - id: f_people + value: "сам заявитель вышел на улицу, соседи выходят" + reveal_on: { question: q_people } + - id: f_caller + value: "Петров Андрей Викторович, телефон 915-204-18-33" + reveal_on: { question: q_caller } + +checklist: + - { id: q_address, fact: f_address } + - { id: q_what_burns, fact: f_what_burns } + - { id: q_people, fact: f_people } + - { id: q_caller, fact: f_caller } + +required_fields: [address, incident_type, description] + +ground_truth: + address: "Москва, улица Академика Королёва, 15" + victims: 0 + +dds_decision: + expected: decline + reason: "дубль: по этому адресу реагирование уже зарегистрировано, бригада работает на месте" + +tree: + pregenerated: false diff --git a/scenarios/dds-decline-out-of-zone.yaml b/scenarios/dds-decline-out-of-zone.yaml new file mode 100644 index 0000000..826b9c0 --- /dev/null +++ b/scenarios/dds-decline-out-of-zone.yaml @@ -0,0 +1,55 @@ +# Эталонный отказ ДДС: не наша зона. Памятка АРМ-112 называет три причины +# «Не принята» — не наша зона, не наша компетенция, дубль (docs/spec/DATASET.md). +# Здесь адрес за пределами Москвы: карточка дошла до московской службы, но +# реагирует служба области. Мотивированный отказ — без отметок; «Принята» +# вопреки эталону — D2. +id: dds-decline-out-of-zone +title: "Возгорание мусора за пределами зоны ответственности" +type: fire +level: L2 +topics: [fires] +modes: [training, exam, self] + +extends: checklists/fire.yaml +signs: [на улице, мусор, открытое пламя] + +persona: + base: calm + arc: + - { stage: registration, mood: calm } + - { stage: interview, mood: calm } + +first_line: "Здравствуйте. У нас во дворе мусорный бак горит." + +facts: + - id: f_what_burns + value: "горит мусорный бак во дворе, пламя открытое, рядом никого" + reveal_on: { question: q_what_burns } + - id: f_address + value: "Московская область, город Химки, Ленинградская улица, 24, двор" + reveal_on: { question: q_address } + - id: f_people + value: пострадавших нет + reveal_on: { question: q_people } + - id: f_caller + value: "Кузнецова Ольга Петровна, телефон 926-310-55-02" + reveal_on: { question: q_caller } + +checklist: + - { id: q_address, fact: f_address } + - { id: q_what_burns, fact: f_what_burns } + - { id: q_people, fact: f_people } + - { id: q_caller, fact: f_caller } + +required_fields: [address, incident_type, description] + +ground_truth: + address: "Московская область, город Химки, Ленинградская улица, 24" + victims: 0 + +dds_decision: + expected: decline + reason: "не наша зона: адрес в Московской области, реагирует служба области" + +tree: + pregenerated: false