lct-52: два сценария с эталонным отказом ДДС — дубль и чужая зона

This commit is contained in:
Ivan Gerasimov 2026-09-27 23:48:03 +03:00
commit 13642ec973
3 changed files with 199 additions and 0 deletions

View file

@ -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"]

View file

@ -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

View file

@ -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