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

122 lines
4.0 KiB
Python

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)
)