feat: init commint
This commit is contained in:
@@ -55,6 +55,8 @@ def test_intake_persists_timeline_creates_unique_task_and_orders_events(monkeypa
|
||||
],
|
||||
"missing_fields": ["category_code", "environment"],
|
||||
"questions": ["请确认受影响机构。"],
|
||||
"blocking_missing_fields": ["environment"],
|
||||
"blocking_questions": ["请确认受影响机构。"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -90,6 +92,16 @@ def test_missing_info_task_completes_after_required_fields_are_filled():
|
||||
publish_intake_agent()
|
||||
user = get_user_model().objects.create_user(username="stage-23-editor")
|
||||
draft = TicketDraft.objects.create(created_by=user, source_text="无法登录")
|
||||
draft.blocking_missing_fields = [
|
||||
"category_code",
|
||||
"problem_performance",
|
||||
"problem_object",
|
||||
"submitter_name",
|
||||
"submitter_org",
|
||||
"environment",
|
||||
]
|
||||
draft.blocking_questions = ["请一次补充问题对象、现象和适用环境。"]
|
||||
draft.save(update_fields=["blocking_missing_fields", "blocking_questions", "updated_at"])
|
||||
run = create_agent_run_for_draft(draft)
|
||||
from apps.tickets.draft_services import _auto_create_ticket
|
||||
|
||||
@@ -218,6 +230,9 @@ def test_task_api_filters_by_ticket(admin_client):
|
||||
publish_intake_agent()
|
||||
user = get_user_model().objects.create_user(username="stage-23-task-api")
|
||||
draft = TicketDraft.objects.create(created_by=user, source_text="无法登录")
|
||||
draft.blocking_missing_fields = ["problem_object"]
|
||||
draft.blocking_questions = ["请确认发生问题的对象。"]
|
||||
draft.save(update_fields=["blocking_missing_fields", "blocking_questions", "updated_at"])
|
||||
run = create_agent_run_for_draft(draft)
|
||||
from apps.tickets.draft_services import _auto_create_ticket
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.management import call_command
|
||||
from django.test import override_settings
|
||||
|
||||
from apps.agents.models import AgentDefinition, AgentRun
|
||||
from apps.agents.tests.helpers import create_deployment
|
||||
@@ -22,14 +23,16 @@ from apps.tickets.support_analysis import (
|
||||
SupportAnalysisError,
|
||||
validate_support_analysis_output,
|
||||
)
|
||||
from apps.tickets.support_contract import SUPPORT_OUTPUT_FIELDS
|
||||
|
||||
|
||||
def publish_support_agent():
|
||||
deployment = create_deployment()
|
||||
deployment = create_deployment(structured_output_mode="native_json_schema")
|
||||
call_command("publish_support_analysis", deployment_id=deployment.id)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(SUPPORT_REFERENCE_MIN_SCORE=0)
|
||||
def test_support_analysis_uses_only_published_scoped_references_and_is_audited(
|
||||
admin_client,
|
||||
django_capture_on_commit_callbacks,
|
||||
@@ -200,6 +203,7 @@ def test_support_analysis_reuses_active_run_and_rejects_closed_ticket(
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(SUPPORT_REFERENCE_MIN_SCORE=0)
|
||||
def test_support_analysis_output_cannot_escape_frozen_references(
|
||||
admin_client,
|
||||
django_capture_on_commit_callbacks,
|
||||
@@ -292,6 +296,7 @@ def test_support_analysis_reports_missing_published_agent(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(SUPPORT_REFERENCE_MIN_SCORE=0)
|
||||
@pytest.mark.parametrize(
|
||||
("citation", "expected_status", "expected_error"),
|
||||
[
|
||||
@@ -360,3 +365,6 @@ def test_publish_support_analysis_command_is_idempotent():
|
||||
assert definition.active_version is not None
|
||||
assert definition.versions.count() == 1
|
||||
assert definition.active_version.steps.count() == 1
|
||||
output_schema = definition.active_version.steps.get().skill_version.output_schema
|
||||
assert set(output_schema["required"]) == SUPPORT_OUTPUT_FIELDS
|
||||
assert set(output_schema["properties"]) == SUPPORT_OUTPUT_FIELDS
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.test import override_settings
|
||||
from apps.agents.models import AgentDefinition, AgentRun
|
||||
from apps.agents.services import build_config_snapshot
|
||||
from apps.agents.tests.helpers import create_deployment
|
||||
from apps.solutions.models import Solution, TicketResolution, TicketSolutionLink
|
||||
from apps.solutions.models import Solution
|
||||
from apps.tickets.models import (
|
||||
CustomerReplyRevision,
|
||||
CustomerReplyTask,
|
||||
@@ -112,11 +112,13 @@ def build_completed_support_analysis(
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(TICKET_AUTO_RESOLVE_ENABLED=True)
|
||||
def test_support_analysis_auto_resolves_through_domain_service_and_is_idempotent(
|
||||
def test_support_analysis_waits_for_one_approval_and_is_idempotent(
|
||||
admin_client,
|
||||
):
|
||||
actor = get_user_model().objects.get(is_superuser=True)
|
||||
analysis, solution, ticket = build_completed_support_analysis(actor)
|
||||
ticket.status = "in_progress"
|
||||
ticket.save(update_fields=["status", "updated_at"])
|
||||
|
||||
process_support_analysis_result(analysis.run)
|
||||
process_support_analysis_result(analysis.run)
|
||||
@@ -124,21 +126,13 @@ def test_support_analysis_auto_resolves_through_domain_service_and_is_idempotent
|
||||
analysis.refresh_from_db()
|
||||
ticket.refresh_from_db()
|
||||
solution.refresh_from_db()
|
||||
resolution = TicketResolution.objects.get(ticket=ticket)
|
||||
assert ticket.status == "resolved"
|
||||
assert resolution.source == "agent"
|
||||
assert resolution.linked_solution == solution
|
||||
assert resolution.agent_run_id == analysis.run_id
|
||||
assert analysis.applied_solution == solution
|
||||
assert analysis.automation_decision["status"] == "auto_resolved"
|
||||
assert analysis.automation_decision["applied"] is True
|
||||
assert solution.usage_count == 1
|
||||
assert TicketSolutionLink.objects.filter(
|
||||
ticket=ticket,
|
||||
solution=solution,
|
||||
link_type="adopted",
|
||||
agent_run_id=analysis.run_id,
|
||||
).count() == 1
|
||||
assert ticket.status == "in_progress"
|
||||
assert not hasattr(ticket, "resolution")
|
||||
assert analysis.applied_solution_id is None
|
||||
assert analysis.automation_decision["status"] == "waiting_approval"
|
||||
assert analysis.automation_decision["applied"] is False
|
||||
assert CustomerReplyTask.objects.filter(ticket=ticket, status="draft").count() == 1
|
||||
assert solution.usage_count == 0
|
||||
assert CustomerReplyTask.objects.filter(ticket=ticket, status="draft").count() == 1
|
||||
assert WorkItem.objects.filter(
|
||||
ticket=ticket,
|
||||
|
||||
@@ -59,7 +59,7 @@ def build_support_analysis(
|
||||
title="订单状态未更新",
|
||||
description="生产环境订单状态长时间未更新",
|
||||
category=category,
|
||||
status="pending",
|
||||
status="in_progress",
|
||||
related_system="订单系统",
|
||||
environment="生产环境",
|
||||
)
|
||||
@@ -222,7 +222,15 @@ def test_stage_3_real_case_metrics_cover_match_auto_reuse_and_fallback():
|
||||
)
|
||||
]
|
||||
|
||||
assert all(result.passed for result in results)
|
||||
for result in results:
|
||||
assert result.passed, {
|
||||
"case": result.case.key,
|
||||
"actual_auto_resolved": result.actual_auto_resolved,
|
||||
"actual_manual_fallback": result.actual_manual_fallback,
|
||||
"matched": result.matched_expected_solution,
|
||||
"citations_valid": result.citations_valid,
|
||||
"decision": result.details["automation_decision"],
|
||||
}
|
||||
metrics = build_support_quality_metrics()
|
||||
assert metrics["evaluation"]["solution_match_rate"] == 1.0
|
||||
assert metrics["evaluation"]["eligible_auto_resolve_rate"] == 1.0
|
||||
|
||||
204
backend/apps/tickets/tests/test_ticket_automation.py
Normal file
204
backend/apps/tickets/tests/test_ticket_automation.py
Normal file
@@ -0,0 +1,204 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user