refactor: запись хода занятия — один commit SessionStore на операцию вместо журнала и checkpoint
Правка балла с пульта и из отчёта идёт одной доменной операцией: раньше WS-путь не обновлял full_report живой сессии.
This commit is contained in:
parent
72c6a7cfa3
commit
47cb85ee02
30 changed files with 1406 additions and 1161 deletions
70
backend/tests/test_session_store.py
Normal file
70
backend/tests/test_session_store.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Одна операция занятия — один commit хранилища, события только после него."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from app.domain.events import CallStarted, Speaker
|
||||
from app.session.hub import LEASE_FENCED_MESSAGE, SessionHub
|
||||
from app.session.state import now_utc
|
||||
from app.session.store import MemorySessionStore, UtteranceAppended
|
||||
from tests.test_session_checkpoint import dds_state
|
||||
|
||||
|
||||
class FailingStore(MemorySessionStore):
|
||||
async def commit(self, state, records=()):
|
||||
raise OSError("simulated database partition")
|
||||
|
||||
|
||||
def test_operation_commits_snapshot_and_records_once_then_publishes():
|
||||
store = MemorySessionStore()
|
||||
local_hub = SessionHub(store=store)
|
||||
state = local_hub.register(dds_state())
|
||||
sid = state.session_id
|
||||
|
||||
with local_hub.trainee(sid) as trainee:
|
||||
async def operation():
|
||||
async with local_hub.operation(sid):
|
||||
entry = state.append(Speaker.OPERATOR, "Адрес?")
|
||||
local_hub.record(sid, UtteranceAppended(entry))
|
||||
local_hub.to_trainee(sid, CallStarted(started_at=now_utc()))
|
||||
assert trainee.empty(), "событие ушло до коммита"
|
||||
assert store.commits == []
|
||||
|
||||
asyncio.run(operation())
|
||||
assert isinstance(trainee.get_nowait(), CallStarted)
|
||||
|
||||
assert len(store.commits) == 1
|
||||
committed_id, records = store.commits[0]
|
||||
assert committed_id == sid
|
||||
assert [type(item) for item in records] == [UtteranceAppended]
|
||||
assert store.snapshot(sid)["transcript"][0]["text"] == "Адрес?"
|
||||
|
||||
|
||||
def test_failed_commit_drops_events_and_fences_session():
|
||||
local_hub = SessionHub(store=FailingStore())
|
||||
state = local_hub.register(dds_state())
|
||||
sid = state.session_id
|
||||
|
||||
with local_hub.observer(sid) as observers, \
|
||||
local_hub.trainee(sid) as trainee, \
|
||||
local_hub.station(sid) as station:
|
||||
async def operation():
|
||||
async with local_hub.operation(sid):
|
||||
local_hub.to_trainee(sid, CallStarted(started_at=now_utc()))
|
||||
|
||||
with pytest.raises(OSError, match="partition"):
|
||||
asyncio.run(operation())
|
||||
|
||||
assert state.lease_fenced
|
||||
assert local_hub.get(sid) is None
|
||||
for queue in (observers, trainee, station):
|
||||
assert queue.get_nowait().message == LEASE_FENCED_MESSAGE
|
||||
assert queue.empty(), "событие несостоявшейся операции утекло"
|
||||
|
||||
|
||||
def test_record_outside_operation_is_rejected():
|
||||
local_hub = SessionHub(store=MemorySessionStore())
|
||||
state = local_hub.register(dds_state())
|
||||
with pytest.raises(RuntimeError):
|
||||
local_hub.record(state.session_id, UtteranceAppended(state.append(Speaker.OPERATOR, "x")))
|
||||
Loading…
Reference in a new issue