100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
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')
|