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