import pytest from django.contrib.auth import get_user_model from apps.channels.models import ChannelConversation, ChannelEndpoint, ChannelMessage from apps.solutions.models import TicketResolution from apps.tickets.automation import ( apply_automation_action, approve_and_send, close_due_ticket_automation, handle_customer_feedback, handle_customer_reply_delivery_failed, ) from apps.tickets.models import ( CustomerReplyTask, Ticket, TicketAutomationEvent, TicketAutomationRun, TicketAutomationRunStatus, ) from apps.workitems.models import WorkItem, WorkItemStatus, WorkItemType @pytest.mark.django_db(transaction=True) def test_one_approval_resolves_and_closes_without_duplicate_business_facts(): actor = get_user_model().objects.create_superuser( username="automation-approver", password="test-password", ) ticket = Ticket.objects.create( ticket_no="TK-AUTO-0001", title="登录回调后仍停留在登录页", description="生产环境登录回调成功,但页面没有跳转。", status="pending_confirm", ) reply_task = CustomerReplyTask.objects.create( ticket=ticket, status="draft", draft_text="您好,请重新登录并确认页面已恢复。", final_text="您好,请重新登录并确认页面已恢复。", revision=1, ) run = TicketAutomationRun.objects.create( ticket=ticket, status=TicketAutomationRunStatus.WAITING_APPROVAL, current_step="approve_and_send", reply_task=reply_task, policy_snapshot={ "policy_version": "test", "auto_close_hours": 24, "max_retries": 2, }, ) approve_and_send( run, actor=actor, expected_reply_revision=1, final_text=reply_task.final_text, resolution_text="认证回调状态已恢复,会话可以正常写入。", manual_delivery_confirmed=True, ) ticket.refresh_from_db() run.refresh_from_db() reply_task.refresh_from_db() assert ticket.status == "resolved" assert reply_task.status == "sent" assert run.status == TicketAutomationRunStatus.WAITING_CUSTOMER assert run.next_action_at is not None assert TicketResolution.objects.filter(ticket=ticket, source="system").count() == 1 assert not WorkItem.objects.filter( ticket=ticket, task_type=WorkItemType.TICKET_CLOSE, status=WorkItemStatus.ACTIVE, ).exists() # 浏览器重复提交相同批准不会产生第二条回复、解决记录或关闭计划。 approve_and_send( run, actor=actor, expected_reply_revision=1, final_text=reply_task.final_text, resolution_text="认证回调状态已恢复,会话可以正常写入。", manual_delivery_confirmed=True, ) assert CustomerReplyTask.objects.filter(ticket=ticket).count() == 1 assert TicketResolution.objects.filter(ticket=ticket).count() == 1 assert TicketAutomationEvent.objects.filter( run=run, event_type="waiting_customer", ).count() == 1 assert close_due_ticket_automation(run.id, force=True) is True assert close_due_ticket_automation(run.id, force=True) is False ticket.refresh_from_db() run.refresh_from_db() assert ticket.status == "closed" assert run.status == TicketAutomationRunStatus.COMPLETED assert TicketAutomationEvent.objects.filter( run=run, event_type="completed", ).count() == 1 @pytest.mark.django_db(transaction=True) def test_negative_feedback_after_close_creates_one_linked_follow_up_ticket(): source = Ticket.objects.create( ticket_no="TK-AUTO-0002", title="登录问题已关闭", description="登录回调问题已经恢复。", status="closed", priority="high", environment="生产环境", related_system="统一身份平台", ) run = TicketAutomationRun.objects.create( ticket=source, status=TicketAutomationRunStatus.COMPLETED, current_step="closed", policy_snapshot={"policy_version": "test"}, ) assert handle_customer_feedback(source.id, "还是不行,登录后继续报错") == ( "follow_up_created" ) assert handle_customer_feedback(source.id, "还是不行,登录后继续报错") == ( "follow_up_created" ) follow_up = Ticket.objects.get(follow_up_of=source) assert follow_up.status == "pending" assert follow_up.priority == "high" assert follow_up.environment == "生产环境" assert source.follow_up_tickets.count() == 1 assert source.events.filter(event_type="follow_up_created").count() == 1 assert run.events.filter(event_type="follow_up_created").count() == 1 @pytest.mark.django_db(transaction=True) def test_failed_channel_delivery_retries_the_same_outbound_message(monkeypatch): actor = get_user_model().objects.create_superuser( username="automation-retry", password="test-password", ) ticket = Ticket.objects.create( ticket_no="TK-AUTO-0003", title="渠道重试", description="验证渠道失败后的幂等重试。", status="pending_confirm", ) reply_task = CustomerReplyTask.objects.create( ticket=ticket, status="approved", draft_text="请确认恢复结果。", final_text="请确认恢复结果。", revision=1, ) run = TicketAutomationRun.objects.create( ticket=ticket, status=TicketAutomationRunStatus.WAITING_DELIVERY, current_step="wait_delivery", reply_task=reply_task, approved_by=actor, policy_snapshot={"policy_version": "test"}, ) endpoint = ChannelEndpoint.objects.create(name="自动化重试渠道") conversation = ChannelConversation.objects.create( endpoint=endpoint, external_conversation_id="retry-conversation", ticket=ticket, ) message = ChannelMessage.objects.create( endpoint=endpoint, conversation=conversation, ticket=ticket, customer_reply_task=reply_task, reply_revision=1, direction="outbound", idempotency_key="automation-retry-message", text=reply_task.final_text, status="failed", error_message="上游超时", created_by=actor, ) queued = [] monkeypatch.setattr( "apps.channels.tasks.deliver_channel_message_task.delay", lambda message_id: queued.append(message_id), ) handle_customer_reply_delivery_failed(reply_task.id, "上游超时") run.refresh_from_db() assert run.current_step == "delivery_failed" assert run.last_error_code == "channel_delivery_failed" apply_automation_action(run, "retry", actor=actor) run.refresh_from_db() message.refresh_from_db() assert message.status == "queued" assert message.idempotency_key == "automation-retry-message" assert run.current_step == "wait_delivery" assert run.last_error_message == "" assert queued == [message.id]