fix: пауза исключена из реакции на доклад
This commit is contained in:
parent
0d58878cbe
commit
6e7c4f4596
8 changed files with 97 additions and 16 deletions
|
|
@ -432,6 +432,8 @@ async def call(ws: WebSocket, session_id: UUID) -> None:
|
||||||
if hub.is_lease_fenced(session_id):
|
if hub.is_lease_fenced(session_id):
|
||||||
await close_fenced(ws)
|
await close_fenced(ws)
|
||||||
return
|
return
|
||||||
|
# Переподключившийся клиент сначала узнаёт, разрешён ли ввод.
|
||||||
|
queue.put_nowait(SessionPaused(paused=state.paused))
|
||||||
if state.exercise is Exercise.CARD:
|
if state.exercise is Exercise.CARD:
|
||||||
from app.api.ws.control import card_briefing
|
from app.api.ws.control import card_briefing
|
||||||
|
|
||||||
|
|
@ -465,9 +467,6 @@ async def call(ws: WebSocket, session_id: UUID) -> None:
|
||||||
if state.recorder is None:
|
if state.recorder is None:
|
||||||
state.recorder = start_recording(session_id)
|
state.recorder = start_recording(session_id)
|
||||||
_start_voice(session_id, state, initial_statement=False)
|
_start_voice(session_id, state, initial_statement=False)
|
||||||
# Каждый новый канал получает серверное состояние, включая снятую паузу:
|
|
||||||
# клиент мог потерять событие resume во время переподключения.
|
|
||||||
queue.put_nowait(SessionPaused(paused=state.paused))
|
|
||||||
writer = asyncio.create_task(pump(ws, queue))
|
writer = asyncio.create_task(pump(ws, queue))
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from app.domain.statuses import (
|
from app.domain.statuses import (
|
||||||
COMMENT_REQUIRED,
|
COMMENT_REQUIRED,
|
||||||
|
|
@ -68,6 +70,7 @@ TERMINAL_REFUSAL = {ServiceStatus.DECLINED, ServiceStatus.REFUSED}
|
||||||
|
|
||||||
def report_reactions(
|
def report_reactions(
|
||||||
marks: list[StatusEntry], reports: list[PhoneReportRecord],
|
marks: list[StatusEntry], reports: list[PhoneReportRecord],
|
||||||
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> list[tuple[PhoneReportRecord, int | None]]:
|
) -> list[tuple[PhoneReportRecord, int | None]]:
|
||||||
"""Задержка статуса после каждого доклада бригады, мс; None — статус не отмечен.
|
"""Задержка статуса после каждого доклада бригады, мс; None — статус не отмечен.
|
||||||
|
|
||||||
|
|
@ -84,15 +87,22 @@ def report_reactions(
|
||||||
result.append((report, 0))
|
result.append((report, 0))
|
||||||
continue
|
continue
|
||||||
after = min(times, default=None)
|
after = min(times, default=None)
|
||||||
result.append((
|
if after is None:
|
||||||
report,
|
result.append((report, None))
|
||||||
int((after - report.at).total_seconds() * 1000) if after is not None else None,
|
continue
|
||||||
))
|
elapsed_ms = int((after - report.at).total_seconds() * 1000)
|
||||||
|
for start, end in pause_intervals or ():
|
||||||
|
overlap_start = max(report.at, start)
|
||||||
|
overlap_end = min(after, end)
|
||||||
|
if overlap_end > overlap_start:
|
||||||
|
elapsed_ms -= int((overlap_end - overlap_start).total_seconds() * 1000)
|
||||||
|
result.append((report, max(0, elapsed_ms)))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def report_reaction(
|
def report_reaction(
|
||||||
service: str, marks: list[StatusEntry], reports: list[PhoneReportRecord], limit_ms: int,
|
service: str, marks: list[StatusEntry], reports: list[PhoneReportRecord], limit_ms: int,
|
||||||
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> tuple[Metric, Finding | None] | None:
|
) -> tuple[Metric, Finding | None] | None:
|
||||||
"""Метрика и отметка D6 по самой поздней реакции службы на доклады бригады.
|
"""Метрика и отметка D6 по самой поздней реакции службы на доклады бригады.
|
||||||
|
|
||||||
|
|
@ -100,7 +110,7 @@ def report_reaction(
|
||||||
где отметки нет. None — реагировать было не на что.
|
где отметки нет. None — реагировать было не на что.
|
||||||
"""
|
"""
|
||||||
refused = current(marks, service) in TERMINAL_REFUSAL
|
refused = current(marks, service) in TERMINAL_REFUSAL
|
||||||
reactions = [(report, delay) for report, delay in report_reactions(marks, reports)
|
reactions = [(report, delay) for report, delay in report_reactions(marks, reports, pause_intervals)
|
||||||
if delay is not None or not refused]
|
if delay is not None or not refused]
|
||||||
if not reactions:
|
if not reactions:
|
||||||
return None
|
return None
|
||||||
|
|
@ -190,6 +200,7 @@ def evaluate_dispatcher(
|
||||||
expected_decision_reason: str | None = None,
|
expected_decision_reason: str | None = None,
|
||||||
phone_reports: list[PhoneReportRecord] | None = None,
|
phone_reports: list[PhoneReportRecord] | None = None,
|
||||||
report_reaction_ms: int = REPORT_REACTION_MS,
|
report_reaction_ms: int = REPORT_REACTION_MS,
|
||||||
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> list[Finding]:
|
) -> list[Finding]:
|
||||||
"""Отметки по работе диспетчера. Пустой список — работа без нарушений.
|
"""Отметки по работе диспетчера. Пустой список — работа без нарушений.
|
||||||
|
|
||||||
|
|
@ -381,7 +392,7 @@ def evaluate_dispatcher(
|
||||||
)
|
)
|
||||||
|
|
||||||
# D6 — статус по докладу бригады отмечен позже лимита или не отмечен.
|
# D6 — статус по докладу бригады отмечен позже лимита или не отмечен.
|
||||||
reaction = report_reaction(service, marks, reports, report_reaction_ms)
|
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
||||||
if reaction is not None and reaction[1] is not None:
|
if reaction is not None and reaction[1] is not None:
|
||||||
findings.append(reaction[1])
|
findings.append(reaction[1])
|
||||||
|
|
||||||
|
|
@ -396,6 +407,7 @@ def dispatcher_metrics(
|
||||||
*,
|
*,
|
||||||
services: list[str] | None = None,
|
services: list[str] | None = None,
|
||||||
report_reaction_ms: int = REPORT_REACTION_MS,
|
report_reaction_ms: int = REPORT_REACTION_MS,
|
||||||
|
pause_intervals: Sequence[tuple[datetime, datetime]] | None = None,
|
||||||
) -> list[Metric]:
|
) -> list[Metric]:
|
||||||
"""Числовая часть оценки ДДС; каждый проверяемый шаг имеет факт и норму.
|
"""Числовая часть оценки ДДС; каждый проверяемый шаг имеет факт и норму.
|
||||||
|
|
||||||
|
|
@ -429,7 +441,7 @@ def dispatcher_metrics(
|
||||||
# Метрика есть только там, где бригада докладывала: без докладов
|
# Метрика есть только там, где бригада докладывала: без докладов
|
||||||
# реагировать не на что, и отсутствие звонков не штраф.
|
# реагировать не на что, и отсутствие звонков не штраф.
|
||||||
reports = [report for report in card.phone_reports if report.service == service]
|
reports = [report for report in card.phone_reports if report.service == service]
|
||||||
reaction = report_reaction(service, marks, reports, report_reaction_ms)
|
reaction = report_reaction(service, marks, reports, report_reaction_ms, pause_intervals)
|
||||||
if reaction is not None:
|
if reaction is not None:
|
||||||
metrics.append(reaction[0])
|
metrics.append(reaction[0])
|
||||||
expected_status = (ServiceStatus.ACCEPTED if expected_decision == "accept"
|
expected_status = (ServiceStatus.ACCEPTED if expected_decision == "accept"
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,12 @@ def score_dds_card(state, card: DdsLiveCard) -> DdsCardRecord:
|
||||||
expected_decision_reason=dds_decision.reason,
|
expected_decision_reason=dds_decision.reason,
|
||||||
phone_reports=card.phone_reports,
|
phone_reports=card.phone_reports,
|
||||||
report_reaction_ms=reaction_ms,
|
report_reaction_ms=reaction_ms,
|
||||||
|
pause_intervals=state.pause_intervals,
|
||||||
)
|
)
|
||||||
metrics = dispatcher_metrics(
|
metrics = dispatcher_metrics(
|
||||||
card, decision_limit_ms, dds_decision.expected, dds_decision.reason,
|
card, decision_limit_ms, dds_decision.expected, dds_decision.reason,
|
||||||
services=services, report_reaction_ms=reaction_ms,
|
services=services, report_reaction_ms=reaction_ms,
|
||||||
|
pause_intervals=state.pause_intervals,
|
||||||
)
|
)
|
||||||
weighted = GostResult(metrics=metrics, findings=findings)
|
weighted = GostResult(metrics=metrics, findings=findings)
|
||||||
work_limit_ms = card.timers.limits[TimerCode.DDS_WORK]
|
work_limit_ms = card.timers.limits[TimerCode.DDS_WORK]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.domain.events import CallEndReason, Exercise, LessonCriteria, Metric
|
from app.domain.events import CallEndReason, Exercise, LessonCriteria, Metric
|
||||||
|
|
@ -68,6 +69,7 @@ class ScoringSnapshot:
|
||||||
#: Карточка на пульте в упражнении по звонку: отметки D1–D6 рядом с E1–E6.
|
#: Карточка на пульте в упражнении по звонку: отметки D1–D6 рядом с E1–E6.
|
||||||
call_card: DdsLiveCard | None
|
call_card: DdsLiveCard | None
|
||||||
call_card_services: tuple[str, ...]
|
call_card_services: tuple[str, ...]
|
||||||
|
pause_intervals: tuple[tuple[datetime, datetime], ...]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reports_cards(self) -> bool:
|
def reports_cards(self) -> bool:
|
||||||
|
|
@ -112,6 +114,7 @@ class ScoringSnapshot:
|
||||||
call_card=live_card.model_copy(deep=True) if live_card is not None else None,
|
call_card=live_card.model_copy(deep=True) if live_card is not None else None,
|
||||||
call_card_services=(tuple(state.card_services(live_card))
|
call_card_services=(tuple(state.card_services(live_card))
|
||||||
if live_card is not None else ()),
|
if live_card is not None else ()),
|
||||||
|
pause_intervals=tuple(state.pause_intervals),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -231,10 +234,12 @@ async def score_session(
|
||||||
expected_decision_reason=scenario.dds_decision.reason,
|
expected_decision_reason=scenario.dds_decision.reason,
|
||||||
phone_reports=live_card.phone_reports,
|
phone_reports=live_card.phone_reports,
|
||||||
report_reaction_ms=reaction_ms,
|
report_reaction_ms=reaction_ms,
|
||||||
|
pause_intervals=snapshot.pause_intervals,
|
||||||
))
|
))
|
||||||
result.metrics.extend(dispatcher_metrics(
|
result.metrics.extend(dispatcher_metrics(
|
||||||
live_card, decision_limit_ms, scenario.dds_decision.expected,
|
live_card, decision_limit_ms, scenario.dds_decision.expected,
|
||||||
scenario.dds_decision.reason, services=services, report_reaction_ms=reaction_ms,
|
scenario.dds_decision.reason, services=services, report_reaction_ms=reaction_ms,
|
||||||
|
pause_intervals=snapshot.pause_intervals,
|
||||||
))
|
))
|
||||||
# Карточки ДДС уже взвешены каждая своим сценарием плюс настройкой
|
# Карточки ДДС уже взвешены каждая своим сценарием плюс настройкой
|
||||||
# занятия. Веса первого сценария поверх них испортили бы остальные.
|
# занятия. Веса первого сценария поверх них испортили бы остальные.
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ class PersistedSession(BaseModel):
|
||||||
paused_at: datetime | None = None
|
paused_at: datetime | None = None
|
||||||
#: Суммарная длительность пауз — для разбора, чтобы время было объяснимо.
|
#: Суммарная длительность пауз — для разбора, чтобы время было объяснимо.
|
||||||
total_paused_ms: int = 0
|
total_paused_ms: int = 0
|
||||||
|
#: Закрытые интервалы пауз нужны для расчёта активного времени между докладом и статусом.
|
||||||
|
pause_intervals: list[tuple[datetime, datetime]] = Field(default_factory=list)
|
||||||
#: Карточка оператора 112, замороженная при передаче в ДДС.
|
#: Карточка оператора 112, замороженная при передаче в ДДС.
|
||||||
dispatched_card: KIO | None = None
|
dispatched_card: KIO | None = None
|
||||||
dispatched_at: datetime | None = None
|
dispatched_at: datetime | None = None
|
||||||
|
|
@ -218,8 +220,11 @@ class SessionState(PersistedSession):
|
||||||
for card in self.desk.cards.values():
|
for card in self.desk.cards.values():
|
||||||
card.timers.resume(now)
|
card.timers.resume(now)
|
||||||
if self.paused_at is not None:
|
if self.paused_at is not None:
|
||||||
idle = now_utc() - self.paused_at
|
resumed_at = now_utc()
|
||||||
|
idle = resumed_at - self.paused_at
|
||||||
self.total_paused_ms += max(0, int(idle.total_seconds() * 1000))
|
self.total_paused_ms += max(0, int(idle.total_seconds() * 1000))
|
||||||
|
if resumed_at > self.paused_at:
|
||||||
|
self.pause_intervals.append((self.paused_at, resumed_at))
|
||||||
if self.desk.next_arrival_at is not None:
|
if self.desk.next_arrival_at is not None:
|
||||||
self.desk.next_arrival_at += idle
|
self.desk.next_arrival_at += idle
|
||||||
self.paused = False
|
self.paused = False
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ def full_state() -> SessionState:
|
||||||
paused=True,
|
paused=True,
|
||||||
paused_at=AT,
|
paused_at=AT,
|
||||||
total_paused_ms=15_000,
|
total_paused_ms=15_000,
|
||||||
|
pause_intervals=[(AT - timedelta(seconds=20), AT - timedelta(seconds=5))],
|
||||||
processed_station_commands=[str(uuid4())],
|
processed_station_commands=[str(uuid4())],
|
||||||
text_revealed_facts={"f_address": "улица Ленина, 14"},
|
text_revealed_facts={"f_address": "улица Ленина, 14"},
|
||||||
consecutive_refusals=2,
|
consecutive_refusals=2,
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ def crew_report(phase: str, seconds: int) -> PhoneReportRecord:
|
||||||
text="доклад", at=at_second(seconds))
|
text="доклад", at=at_second(seconds))
|
||||||
|
|
||||||
|
|
||||||
def reaction(status_log, reports, limit_ms=45_000):
|
def reaction(status_log, reports, limit_ms=45_000, pause_intervals=None):
|
||||||
card = live_card(status_log=status_log, crew_assignments={SERVICES[0]: "Бригада 12"},
|
card = live_card(status_log=status_log, crew_assignments={SERVICES[0]: "Бригада 12"},
|
||||||
dispatched_at=T0)
|
dispatched_at=T0)
|
||||||
card.phone_reports = reports
|
card.phone_reports = reports
|
||||||
|
|
@ -324,9 +324,11 @@ def reaction(status_log, reports, limit_ms=45_000):
|
||||||
entries=status_log, services=SERVICES, crew_assignments=card.crew_assignments,
|
entries=status_log, services=SERVICES, crew_assignments=card.crew_assignments,
|
||||||
deadline_ms=30_000, elapsed_ms=5_000, phone_reports=reports,
|
deadline_ms=30_000, elapsed_ms=5_000, phone_reports=reports,
|
||||||
report_reaction_ms=limit_ms,
|
report_reaction_ms=limit_ms,
|
||||||
|
pause_intervals=pause_intervals,
|
||||||
)
|
)
|
||||||
metric = next((m for m in dispatcher_metrics(card, 30_000, services=SERVICES,
|
metric = next((m for m in dispatcher_metrics(card, 30_000, services=SERVICES,
|
||||||
report_reaction_ms=limit_ms)
|
report_reaction_ms=limit_ms,
|
||||||
|
pause_intervals=pause_intervals)
|
||||||
if m.key == "dds_report_reaction"), None)
|
if m.key == "dds_report_reaction"), None)
|
||||||
return [f for f in findings if f.metric_key == "dds_report_reaction"], metric
|
return [f for f in findings if f.metric_key == "dds_report_reaction"], metric
|
||||||
|
|
||||||
|
|
@ -364,6 +366,21 @@ def test_status_later_than_limit_after_crew_report_is_d6_with_fact_and_norm():
|
||||||
assert metric.credit == time_credit(70_000, 45_000)
|
assert metric.credit == time_credit(70_000, 45_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pause_between_report_and_status_does_not_count_as_reaction_time():
|
||||||
|
marks = full_run(arrived_at=180)
|
||||||
|
reports = [crew_report("arrived", 100)]
|
||||||
|
pause = [(at_second(110), at_second(170))]
|
||||||
|
|
||||||
|
without_pause, late = reaction(marks, reports)
|
||||||
|
with_pause, active = reaction(marks, reports, pause_intervals=pause)
|
||||||
|
|
||||||
|
assert [finding.code for finding in without_pause] == [ErrorCode.D6]
|
||||||
|
assert not late.passed
|
||||||
|
assert with_pause == []
|
||||||
|
assert active.passed and active.credit == time_credit(20_000, 45_000)
|
||||||
|
assert "20 с" in active.fact
|
||||||
|
|
||||||
|
|
||||||
def test_violation_by_a_fraction_of_a_second_is_not_shown_as_the_limit():
|
def test_violation_by_a_fraction_of_a_second_is_not_shown_as_the_limit():
|
||||||
status_log = full_run(arrived_at=170)
|
status_log = full_run(arrived_at=170)
|
||||||
status_log[2] = status_log[2].model_copy(
|
status_log[2] = status_log[2].model_copy(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
@ -41,14 +41,14 @@ from app.domain.events import (
|
||||||
TtsEnd,
|
TtsEnd,
|
||||||
)
|
)
|
||||||
from app.domain.roles import Role
|
from app.domain.roles import Role
|
||||||
from app.domain.statuses import ServiceStatus
|
from app.domain.statuses import PhoneReportRecord, ServiceStatus
|
||||||
from app.domain.timers import TimerCode
|
from app.domain.timers import TimerCode
|
||||||
from app.main import app
|
from app.main import app
|
||||||
from app.scenarios.loader import load_file
|
from app.scenarios.loader import load_file
|
||||||
from app.scoring.dispatcher import dispatcher_metrics
|
from app.scoring.dispatcher import dispatcher_metrics
|
||||||
from app.session import timers as timers_module
|
from app.session import timers as timers_module
|
||||||
from app.session.checkpoint import dump_state, load_state
|
from app.session.checkpoint import dump_state, load_state
|
||||||
from app.session.finish import end_session
|
from app.session.finish import end_session, score_dds_card
|
||||||
from app.session.hub import hub
|
from app.session.hub import hub
|
||||||
from app.session.state import SessionState, now_utc
|
from app.session.state import SessionState, now_utc
|
||||||
from app.session.store import LessonPaused, LessonResumed, MemorySessionStore
|
from app.session.store import LessonPaused, LessonResumed, MemorySessionStore
|
||||||
|
|
@ -621,9 +621,49 @@ def test_repeated_pause_and_resume_are_idempotent(clock, monkeypatch):
|
||||||
assert sum(isinstance(record, LessonResumed) for record in records) == 1
|
assert sum(isinstance(record, LessonResumed) for record in records) == 1
|
||||||
assert [event.paused for event in events if isinstance(event, SessionPaused)] == [True, False]
|
assert [event.paused for event in events if isinstance(event, SessionPaused)] == [True, False]
|
||||||
assert state.total_paused_ms == 60_000
|
assert state.total_paused_ms == 60_000
|
||||||
|
assert state.pause_intervals == [(base, base + timedelta(seconds=60))]
|
||||||
assert card.timers.timers[TimerCode.DDS_ACK].current_ms(clock.now) == 15_000
|
assert card.timers.timers[TimerCode.DDS_ACK].current_ms(clock.now) == 15_000
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_reaction_excludes_pause_after_control_and_checkpoint(clock, monkeypatch):
|
||||||
|
base = datetime(2026, 9, 27, 10, tzinfo=UTC)
|
||||||
|
state, card = _dds_state_with_active_card(clock)
|
||||||
|
service = state.card_services(card)[0]
|
||||||
|
monkeypatch.setattr("app.session.dds.now_utc", lambda: base)
|
||||||
|
card.set_service_status(service, ServiceStatus.ACCEPTED, "Карточка принята")
|
||||||
|
card.phone_reports.append(PhoneReportRecord(
|
||||||
|
service=service, crew="Бригада 12", phase="dispatched",
|
||||||
|
text="Бригада выехала", at=base,
|
||||||
|
))
|
||||||
|
store = MemorySessionStore()
|
||||||
|
monkeypatch.setattr(hub, "store", store)
|
||||||
|
monkeypatch.setitem(hub._sessions, state.session_id, state)
|
||||||
|
|
||||||
|
async def command(event):
|
||||||
|
async with hub.operation(state.session_id):
|
||||||
|
await control_ws._command(state.session_id, event, TEACHER)
|
||||||
|
|
||||||
|
monkeypatch.setattr("app.session.state.now_utc", lambda: base + timedelta(seconds=10))
|
||||||
|
asyncio.run(command(SessionPause()))
|
||||||
|
restored = load_state(dump_state(state), base + timedelta(seconds=70))
|
||||||
|
assert restored.paused and restored.pause_intervals == []
|
||||||
|
monkeypatch.setitem(hub._sessions, state.session_id, restored)
|
||||||
|
monkeypatch.setattr("app.session.state.now_utc", lambda: base + timedelta(seconds=70))
|
||||||
|
asyncio.run(command(SessionResume()))
|
||||||
|
monkeypatch.setattr("app.session.dds.now_utc", lambda: base + timedelta(seconds=80))
|
||||||
|
restored.desk.active.set_service_status(
|
||||||
|
service, ServiceStatus.RESPONDING, "Основание: доклад. Сведения: бригада выехала.",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = score_dds_card(restored, restored.desk.active)
|
||||||
|
reaction = next(item for item in result.metrics if item.key == "dds_report_reaction")
|
||||||
|
assert restored.pause_intervals == [
|
||||||
|
(base + timedelta(seconds=10), base + timedelta(seconds=70)),
|
||||||
|
]
|
||||||
|
assert reaction.passed and "20 с" in reaction.fact
|
||||||
|
assert not any(item.metric_key == "dds_report_reaction" for item in result.findings)
|
||||||
|
|
||||||
|
|
||||||
def test_pause_is_ignored_in_ended_session(clock, monkeypatch):
|
def test_pause_is_ignored_in_ended_session(clock, monkeypatch):
|
||||||
state, _card = _dds_state_with_active_card(clock)
|
state, _card = _dds_state_with_active_card(clock)
|
||||||
store = MemorySessionStore()
|
store = MemorySessionStore()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue