Files
group_fqcd_jr/tests/unit/service/test_agent_persistence_handover.py
T

122 lines
4.0 KiB
Python
Raw Normal View History

2026-09-10 19:51:08 +08:00
from datetime import UTC, datetime
from typing import Any
import pytest
from app.core.contracts import AgentResult, CoreResult, IntentResult
from app.model.conversation import ConversationMessage
from app.model.platform import AgentRun, DomainEventOutbox, HandoverTicket
from app.service.agent_persistence_service import AgentPersistenceService
class FakeTransaction:
async def __aenter__(self) -> None:
return None
async def __aexit__(
self, exc_type: object, exc_value: object, traceback: object
) -> bool:
return False
class FakeSession:
"""仅收集持久化服务在同一事务内计划写入的 ORM 实体。"""
def __init__(self, run: AgentRun) -> None:
self.run = run
self.added: list[Any] = []
self.executed: list[Any] = []
def begin(self) -> FakeTransaction:
return FakeTransaction()
async def scalar(self, statement: object) -> AgentRun:
return self.run
def add(self, item: Any) -> None:
self.added.append(item)
async def flush(self) -> None:
for item in self.added:
if isinstance(item, ConversationMessage) and item.id is None:
item.id = 901
async def execute(self, statement: object) -> None:
self.executed.append(statement)
def queued_run() -> AgentRun:
now = datetime.now(UTC).replace(tzinfo=None)
return AgentRun(
id=1,
run_id="run-transfer-1",
idempotency_id=2,
session_id="session-transfer-1",
user_id=7,
agent_type="customer_service",
trace_id="trace-transfer-1",
request_message_id=800,
status="queued",
created_at=now,
updated_at=now,
)
def result(*, transfer_required: bool) -> AgentResult:
return AgentResult(
run_id="run-transfer-1",
result=CoreResult(
text="已为您转接人工客服。",
intent=IntentResult(intent="human_handover", confidence=1),
transfer_required=transfer_required,
transfer_reason="user_requested" if transfer_required else None,
),
)
def added_of(items: list[Any], model: type[Any]) -> list[Any]:
return [item for item in items if isinstance(item, model)]
@pytest.mark.asyncio
async def test_transfer_required_result_creates_pending_ticket_and_outbox_event() -> None:
"""Agent 发起的转人工必须在完成运行的事务内留待处理工单和通知事件。"""
session = FakeSession(queued_run())
await AgentPersistenceService(session).complete_run(
"run-transfer-1", result(transfer_required=True), memory_extraction_requested=False
)
tickets = added_of(session.added, HandoverTicket)
events = added_of(session.added, DomainEventOutbox)
assert len(tickets) == 1
assert tickets[0].status == "pending"
assert tickets[0].session_id == "session-transfer-1"
assert tickets[0].customer_id == 7
assert tickets[0].source_agent == "customer_service"
assert tickets[0].source_message_id == 901
assert tickets[0].reason_code == "user_requested"
assert len(events) == 2 # agent.run_completed + conversation.transfer_requested
transfer_event = next(
event for event in events if event.event_type == "conversation.transfer_requested"
)
assert transfer_event.aggregate_type == "conversation"
assert transfer_event.aggregate_id == "session-transfer-1"
assert transfer_event.payload["ticket_no"] == tickets[0].ticket_no
@pytest.mark.asyncio
async def test_normal_result_does_not_create_handover_ticket_or_event() -> None:
"""非转人工回答不得污染管理员待处理队列。"""
session = FakeSession(queued_run())
await AgentPersistenceService(session).complete_run(
"run-transfer-1", result(transfer_required=False), memory_extraction_requested=False
)
assert added_of(session.added, HandoverTicket) == []
assert all(
event.event_type != "conversation.transfer_requested"
for event in added_of(session.added, DomainEventOutbox)
)