import asyncio from types import SimpleNamespace from unittest.mock import AsyncMock, Mock import pytest from src.core import fastgpt_client as wiring from src.services.app_service import AccidentAppService from src.services.repository import CheckpointNotFoundError, InMemorySessionRepository @pytest.mark.asyncio @pytest.mark.parametrize('error', [ ConnectionError('storage offline'), TimeoutError('storage timeout'), RuntimeError('no such thread'), KeyError('storage schema'), ]) async def test_checkpoint_failure_keeps_business_fields_and_allows_retry(error): repo = InMemorySessionRepository() service = AccidentAppService(SimpleNamespace(), repo) await service.set_info('s', 'hphm1', 'test-plate') drop = AsyncMock(side_effect=error) with pytest.raises(type(error)): await service.delete_session('s', drop) assert await service.get_info('s', 'hphm1') == '"test-plate"' assert not service._locks._entries drop.side_effect = None await service.delete_session('s', drop) assert await service.get_info('s', 'hphm1') == '""' @pytest.mark.asyncio async def test_explicit_missing_checkpoint_is_idempotent(): service = AccidentAppService(SimpleNamespace(), InMemorySessionRepository()) await service.set_info('s', 'hphm1', 'test-plate') drop = AsyncMock(side_effect=CheckpointNotFoundError()) await service.delete_session('s', drop) await service.delete_session('s', drop) assert await service.get_info('s', 'hphm1') == '""' assert drop.await_count == 2 @pytest.mark.asyncio async def test_cancelled_checkpoint_cleanup_keeps_business_fields(): service = AccidentAppService(SimpleNamespace(), InMemorySessionRepository()) await service.set_info('s', 'hphm1', 'test-plate') started = asyncio.Event() async def drop(_): started.set() await asyncio.Future() task = asyncio.create_task(service.delete_session('s', drop)) await asyncio.wait_for(started.wait(), 1) task.cancel() with pytest.raises(asyncio.CancelledError): await task assert await service.get_info('s', 'hphm1') == '"test-plate"' assert not service._locks._entries @pytest.mark.asyncio async def test_business_delete_failure_is_not_reported_as_success(): class FailingRepository(InMemorySessionRepository): def delete(self, _): raise ConnectionError('business storage offline') service = AccidentAppService(SimpleNamespace(), FailingRepository()) await service.set_info('s', 'hphm1', 'test-plate') drop = AsyncMock() with pytest.raises(ConnectionError): await service.delete_session('s', drop) drop.assert_awaited_once_with('s') assert await service.get_info('s', 'hphm1') == '"test-plate"' @pytest.mark.asyncio async def test_checkpoint_cleanup_uses_async_api_and_propagates_errors(monkeypatch): saver = SimpleNamespace(adelete_thread=AsyncMock(side_effect=ConnectionError()), delete_thread=Mock()) monkeypatch.setattr(wiring, '_langgraph_graph', SimpleNamespace(checkpointer=saver)) with pytest.raises(ConnectionError): await wiring.delete_chat_session('s') saver.adelete_thread.assert_awaited_once_with('s') saver.delete_thread.assert_not_called() @pytest.mark.asyncio @pytest.mark.parametrize('graph', [None, SimpleNamespace(), SimpleNamespace(checkpointer=object())]) async def test_missing_cleanup_capability_fails(monkeypatch, graph): monkeypatch.setattr(wiring, '_langgraph_graph', graph) with pytest.raises(RuntimeError): await wiring.delete_chat_session('s') @pytest.mark.asyncio async def test_sync_checkpoint_cleanup_is_supported_and_errors_propagate(monkeypatch): saver = SimpleNamespace(delete_thread=Mock()) monkeypatch.setattr(wiring, '_langgraph_graph', SimpleNamespace(checkpointer=saver)) await wiring.delete_chat_session('s') saver.delete_thread.assert_called_once_with('s') saver.delete_thread.side_effect = ConnectionError() with pytest.raises(ConnectionError): await wiring.delete_chat_session('s')