Files
ZNJJ-api-server/test/services/test_delete_failure_semantics.py
T

100 lines
4.0 KiB
Python
Raw Normal View History

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):
async 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')