Files
operating-room-monitor-server/tests/test_surgery_pipeline_persistence.py
Kevin 8a4bad99d3 feat: 配置写死与 baked 模块,Alembic 建表,百度仅 BAIDU_*
- 新增 app/baked/algorithm|pipeline,非部署参数不再走 env;Settings 保留 DB/HTTP/RTSP/海康/百度/MinIO/Demo
- 移除 init_db_schema 与 reload 配置;main 仅 check_database;start*.sh 在 uvicorn 前执行 alembic upgrade head
- 依赖 psycopg[binary] 供 Alembic 同步 URL;alembic/env 注释与预发清单更新
- 撕段门控消费管线、各视频/语音/归档调用改为 baked
- 百度环境变量仅 BAIDU_APP_ID、BAIDU_API_KEY、BAIDU_SECRET_KEY 与 BAIDU_* 超时/ASR;人脸脚本与 baidu_speech 文案同步
- 全量单测与 .env.example 更新;.gitignore 忽略 refs/(本地权重/视频不入库)

Made-with: Cursor
2026-04-24 15:33:22 +08:00

202 lines
6.3 KiB
Python

"""Surgery stop -> DB persist, archive retry, and SurgeryPipeline result resolution order."""
from __future__ import annotations
import asyncio
from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.baked import pipeline as bp
from app.config import Settings
from app.domain.consumption import SurgeryConsumptionStored
from app.repositories.surgery_results import SurgeryResultRepository
from app.services.surgery_pipeline import SurgeryPipeline
from app.services.video.session_manager import (
CameraSessionManager,
RunningSurgery,
SurgerySessionState,
)
from app.services.voice_resolution import VoiceConfirmationService
def _install_active(mgr: CameraSessionManager, surgery_id: str, state: SurgerySessionState) -> None:
"""测试辅助:直接把一条 RunningSurgery 塞进注册表,跳过真实 camera worker。"""
mgr._registry._active[surgery_id] = RunningSurgery(
stop_event=asyncio.Event(), state=state, tasks=[]
)
@pytest.mark.asyncio
async def test_stop_surgery_persists_final_result(
sqlite_session_factory: async_sessionmaker[AsyncSession],
) -> None:
repo = SurgeryResultRepository()
settings = Settings()
mgr = CameraSessionManager(
settings=settings,
vision_algorithm=MagicMock(),
hikvision_runtime=None,
result_repository=repo,
session_factory=sqlite_session_factory,
)
ts = datetime(2026, 4, 21, 12, 0, tzinfo=timezone.utc)
st = SurgerySessionState(candidate_consumables=["纱布"])
st.details.append(
SurgeryConsumptionStored(
item_id="纱布",
item_name="纱布",
qty=1,
doctor_id="vision",
timestamp=ts,
source="vision",
)
)
st.ready.set()
_install_active(mgr, "123456", st)
await mgr.stop_surgery("123456", require_active=True)
async with sqlite_session_factory() as session:
async with session.begin():
loaded = await repo.load_final_details(session, "123456")
assert loaded is not None
assert len(loaded) == 1
assert loaded[0].item_id == "纱布"
assert mgr.archived_consumption_fallback("123456") is None
class _FlakyResultRepo(SurgeryResultRepository):
def __init__(self) -> None:
super().__init__()
self.calls = 0
async def save_final_result(self, session: AsyncSession, **kwargs: object) -> None:
self.calls += 1
if self.calls == 1:
raise RuntimeError("db unavailable")
return await super().save_final_result(session, **kwargs)
@pytest.mark.asyncio
async def test_stop_surgery_failed_persist_goes_to_archive_then_retry_persists(
sqlite_session_factory: async_sessionmaker[AsyncSession],
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo = _FlakyResultRepo()
settings = Settings()
monkeypatch.setattr(
bp,
"ARCHIVE_PERSIST_DURABLE_FALLBACK_DIR",
str(tmp_path / "pending_archive"),
)
mgr = CameraSessionManager(
settings=settings,
vision_algorithm=MagicMock(),
hikvision_runtime=None,
result_repository=repo,
session_factory=sqlite_session_factory,
)
ts = datetime(2026, 4, 21, 12, 0, tzinfo=timezone.utc)
st = SurgerySessionState(candidate_consumables=[])
st.details.append(
SurgeryConsumptionStored(
item_id="缝线",
item_name="缝线",
qty=1,
doctor_id="vision",
timestamp=ts,
source="vision",
)
)
_install_active(mgr, "654321", st)
await mgr.stop_surgery("654321", require_active=True)
assert mgr.archived_consumption_fallback("654321") is not None
assert repo.calls == 1
# durable fallback 文件应已写入
durable = tmp_path / "pending_archive" / "654321.json"
assert durable.exists()
ok = await mgr._archive.try_persist_archive("654321")
assert ok is True
assert mgr.archived_consumption_fallback("654321") is None
assert repo.calls == 2
assert not durable.exists(), "成功落库后 durable 文件应被清理"
async with sqlite_session_factory() as session:
async with session.begin():
loaded = await repo.load_final_details(session, "654321")
assert loaded is not None
assert len(loaded) == 1
assert loaded[0].item_id == "缝线"
@pytest.mark.asyncio
async def test_pipeline_prefers_live_then_db_then_archive(
sqlite_session_factory: async_sessionmaker[AsyncSession],
) -> None:
repo = SurgeryResultRepository()
settings = Settings()
mgr = CameraSessionManager(
settings=settings,
vision_algorithm=MagicMock(),
hikvision_runtime=None,
result_repository=repo,
session_factory=sqlite_session_factory,
)
voice = MagicMock(spec=VoiceConfirmationService)
pipeline = SurgeryPipeline(
mgr,
result_repository=repo,
voice_confirmation=voice,
session_factory=sqlite_session_factory,
)
ts = datetime(2026, 4, 21, 12, 0, tzinfo=timezone.utc)
st = SurgerySessionState(candidate_consumables=["纱布"])
st.details.append(
SurgeryConsumptionStored(
item_id="纱布",
item_name="纱布",
qty=1,
doctor_id="vision",
timestamp=ts,
source="vision",
)
)
st.ready.set()
_install_active(mgr, "111111", st)
live = await pipeline.get_consumption_details_for_client("111111")
assert live is not None
assert live[0].item_id == "纱布"
await mgr.stop_surgery("111111", require_active=True)
from_db = await pipeline.get_consumption_details_for_client("111111")
assert from_db is not None
assert len(from_db) == 1
assert from_db[0].item_id == "纱布"
await mgr._archive.restore(
"333333",
[
SurgeryConsumptionStored(
item_id="归档项",
item_name="归档项",
qty=1,
doctor_id="vision",
timestamp=ts,
source="vision",
)
],
)
only_archive = await pipeline.get_consumption_details_for_client("333333")
assert only_archive is not None
assert only_archive[0].item_id == "归档项"