Files
MoviePilot/tests/test_agent_chat_transaction.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

144 lines
4.7 KiB
Python

"""
Agent 会话删除的请求级事务边界测试。
"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from app.application.messaging.chat import AgentChatService
def _principal() -> SimpleNamespace:
"""
构造拥有目标会话的普通用户。
"""
return SimpleNamespace(id=1, name="alice", is_superuser=False)
def _chat() -> SimpleNamespace:
"""
构造应用服务投影所需的最小会话记录。
"""
return SimpleNamespace(
id=9,
session_id="session-9",
client_session_id=None,
title="事务会话",
channel=None,
source=None,
user_id="1",
username="alice",
original_chat_id=None,
message_count=0,
created_at=None,
updated_at=None,
display_messages=[],
agent_messages=[],
)
@pytest.mark.asyncio
async def test_delete_stages_then_commits_once() -> None:
"""
有权限的会话删除只能由 Service 在暂存成功后提交一次。
"""
calls: list[str] = []
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock(
side_effect=lambda **_kwargs: calls.append("stage") or True
)
unit_of_work = Mock()
unit_of_work.commit = AsyncMock(
side_effect=lambda: calls.append("commit")
)
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
assert await service.delete("session-9", _principal()) is True
assert calls == ["stage", "commit"]
unit_of_work.rollback.assert_not_awaited()
@pytest.mark.asyncio
async def test_delete_rolls_back_flush_failure() -> None:
"""
暂存删除失败必须回滚并传播原异常。
"""
error = RuntimeError("flush failed")
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock(side_effect=error)
unit_of_work = Mock()
unit_of_work.commit = AsyncMock()
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
with pytest.raises(RuntimeError) as raised:
await service.delete("session-9", _principal())
assert raised.value is error
unit_of_work.commit.assert_not_awaited()
unit_of_work.rollback.assert_awaited_once_with()
@pytest.mark.asyncio
async def test_delete_missing_chat_does_not_open_write_transaction() -> None:
"""
会话不存在时保持旧 False 返回,且不执行 stage 或 commit。
"""
repository = Mock()
repository.async_get = AsyncMock(return_value=None)
repository.async_stage_delete = AsyncMock()
unit_of_work = Mock()
unit_of_work.commit = AsyncMock()
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
assert await service.delete("missing", _principal()) is False
repository.async_stage_delete.assert_not_awaited()
unit_of_work.commit.assert_not_awaited()
unit_of_work.rollback.assert_not_awaited()
@pytest.mark.asyncio
async def test_explicit_delete_revokes_independent_history_before_main_commit() -> None:
"""
跨库删除先收回证据,主库提交失败重试也不能让私人历史继续可读。
"""
calls: list[str] = []
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock(side_effect=lambda **_kwargs: calls.append('stage') or True)
unit_of_work = Mock()
unit_of_work.commit = AsyncMock(side_effect=RuntimeError('commit unavailable'))
unit_of_work.rollback = AsyncMock()
recall = Mock()
recall.delete = AsyncMock(side_effect=lambda *_args: calls.append('recall'))
service = AgentChatService(repository, unit_of_work, recall=recall)
with pytest.raises(RuntimeError, match='commit unavailable'):
await service.delete('session-9', _principal())
assert calls == ['recall', 'stage']
recall.delete.assert_awaited_once_with('1', 'session-9')
unit_of_work.rollback.assert_awaited_once()
@pytest.mark.asyncio
async def test_delete_does_not_claim_success_when_evidence_cannot_be_revoked() -> None:
"""
独立库未删除成功时保留主库入口并报告失败,允许用户重试。
"""
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock()
recall = Mock()
recall.delete = AsyncMock(side_effect=OSError('history unavailable'))
service = AgentChatService(repository, Mock(), recall=recall)
with pytest.raises(OSError):
await service.delete('session-9', _principal())
repository.async_stage_delete.assert_not_awaited()