"""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.config import Settings from app.repositories.surgery_results import SurgeryResultRepository from app.schemas import SurgeryConsumptionStored from app.services.surgery_pipeline import SurgeryPipeline from app.services.video.session_manager import ( ArchivedSurgery, CameraSessionManager, RunningSurgery, SurgerySessionState, ) from app.services.voice_resolution import VoiceConfirmationService def _patch_db_sessions( sqlite_session_factory: async_sessionmaker[AsyncSession], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr( "app.services.video.session_manager.AsyncSessionLocal", sqlite_session_factory, ) monkeypatch.setattr( "app.services.surgery_pipeline.AsyncSessionLocal", sqlite_session_factory, ) @pytest.mark.asyncio async def test_stop_surgery_persists_final_result( sqlite_session_factory: async_sessionmaker[AsyncSession], monkeypatch: pytest.MonkeyPatch, ) -> None: _patch_db_sessions(sqlite_session_factory, monkeypatch) repo = SurgeryResultRepository() settings = Settings() mgr = CameraSessionManager( settings=settings, vision_algorithm=MagicMock(), hikvision_runtime=None, result_repository=repo, ) 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() mgr._active["123456"] = RunningSurgery( stop_event=asyncio.Event(), state=st, tasks=[] ) 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._archive.get("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], monkeypatch: pytest.MonkeyPatch, ) -> None: _patch_db_sessions(sqlite_session_factory, monkeypatch) repo = _FlakyResultRepo() settings = Settings() mgr = CameraSessionManager( settings=settings, vision_algorithm=MagicMock(), hikvision_runtime=None, result_repository=repo, ) 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", ) ) mgr._active["654321"] = RunningSurgery( stop_event=asyncio.Event(), state=st, tasks=[] ) await mgr.stop_surgery("654321", require_active=True) assert "654321" in mgr._archive assert repo.calls == 1 ok = await mgr._try_persist_archive("654321") assert ok is True assert "654321" not in mgr._archive assert repo.calls == 2 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], monkeypatch: pytest.MonkeyPatch, ) -> None: _patch_db_sessions(sqlite_session_factory, monkeypatch) repo = SurgeryResultRepository() settings = Settings() mgr = CameraSessionManager( settings=settings, vision_algorithm=MagicMock(), hikvision_runtime=None, result_repository=repo, ) voice = MagicMock(spec=VoiceConfirmationService) pipeline = SurgeryPipeline( mgr, result_repository=repo, voice_confirmation=voice, ) 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() mgr._active["111111"] = RunningSurgery( stop_event=asyncio.Event(), state=st, tasks=[] ) 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 == "纱布" mgr._archive["333333"] = ArchivedSurgery( details=[ 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 == "归档项"