feat: consolidate ticket domain baseline
This commit is contained in:
@@ -3,7 +3,14 @@ from datetime import timedelta
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.tickets.models import Ticket, TicketCategory, TicketEvent
|
||||
from apps.solutions.models import TicketResolution
|
||||
from apps.tickets.models import (
|
||||
Ticket,
|
||||
TicketCategory,
|
||||
TicketCollaborator,
|
||||
TicketComment,
|
||||
TicketEvent,
|
||||
)
|
||||
|
||||
|
||||
def test_create_ticket_returns_ticket_detail(admin_client, db):
|
||||
@@ -205,8 +212,8 @@ def test_business_ticket_can_be_resolved_directly_with_resolution(admin_client,
|
||||
assert response.status_code == 200
|
||||
ticket.refresh_from_db()
|
||||
assert ticket.status == "resolved"
|
||||
assert ticket.resolution_summary == "已查询采集进度,当前已完成 80%。"
|
||||
assert ticket.is_resolved is True
|
||||
assert ticket.resolution.solution_text == "已查询采集进度,当前已完成 80%。"
|
||||
assert response.json()["is_resolved"] is True
|
||||
assert ticket.replied_to_customer is True
|
||||
|
||||
|
||||
@@ -238,20 +245,19 @@ def test_closing_resolved_ticket_keeps_resolved_flag(admin_client, db):
|
||||
title="已解决待关闭",
|
||||
description="A",
|
||||
status="resolved",
|
||||
is_resolved=True,
|
||||
resolution_summary="已完成处理",
|
||||
)
|
||||
TicketResolution.objects.create(ticket=ticket, solution_text="已完成处理")
|
||||
|
||||
response = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/status",
|
||||
data={"status": "closed", "remark": "已回复", "is_resolved": False},
|
||||
data={"status": "closed", "remark": "已回复"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
ticket.refresh_from_db()
|
||||
assert ticket.status == "closed"
|
||||
assert ticket.is_resolved is True
|
||||
assert response.json()["is_resolved"] is True
|
||||
|
||||
|
||||
def test_get_ticket_events_returns_real_timeline_with_operator(admin_client, db):
|
||||
@@ -287,3 +293,88 @@ def test_get_ticket_events_returns_real_timeline_with_operator(admin_client, db)
|
||||
assert payload[1]["to_value"] == {"status": "in_progress"}
|
||||
assert payload[1]["remark"] == "开始排查认证链路"
|
||||
assert payload[1]["operator"]["display_name"] == "admin"
|
||||
|
||||
|
||||
def test_ticket_comments_return_history_author_and_validate_visibility(admin_client, db):
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607270301",
|
||||
title="评论历史",
|
||||
description="需要协同排查",
|
||||
)
|
||||
|
||||
created = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/comments",
|
||||
data={
|
||||
"content": " 已检查应用日志,等待数据库同事反馈。 ",
|
||||
"comment_type": "internal_note",
|
||||
"visibility": "internal",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert created.status_code == 200
|
||||
assert created.json()["content"] == "已检查应用日志,等待数据库同事反馈。"
|
||||
assert created.json()["created_by"]["username"] == "admin"
|
||||
history = admin_client.get(f"/api/tickets/{ticket.id}/comments")
|
||||
assert history.status_code == 200
|
||||
assert history.json()[0]["created_by"]["display_name"] == "admin"
|
||||
assert TicketEvent.objects.filter(ticket=ticket, event_type="commented").count() == 1
|
||||
|
||||
mismatched = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/comments",
|
||||
data={
|
||||
"content": "客户可见内容",
|
||||
"comment_type": "user_reply",
|
||||
"visibility": "internal",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
assert mismatched.status_code == 400
|
||||
assert TicketComment.objects.filter(ticket=ticket).count() == 1
|
||||
|
||||
|
||||
def test_ticket_collaboration_lists_active_users_and_is_idempotent(admin_client, db):
|
||||
user_model = get_user_model()
|
||||
active_user = user_model.objects.create_user(
|
||||
username="collaborator",
|
||||
first_name="协作成员",
|
||||
is_active=True,
|
||||
)
|
||||
user_model.objects.create_user(username="disabled-collaborator", is_active=False)
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607270302",
|
||||
title="协作管理",
|
||||
description="需要多人处理",
|
||||
)
|
||||
|
||||
participants = admin_client.get("/api/tickets/participants")
|
||||
assert participants.status_code == 200
|
||||
usernames = {item["username"] for item in participants.json()}
|
||||
assert "collaborator" in usernames
|
||||
assert "disabled-collaborator" not in usernames
|
||||
|
||||
payload = {"user_id": active_user.id, "role": "collaborator"}
|
||||
first = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/collaborators",
|
||||
data=payload,
|
||||
content_type="application/json",
|
||||
)
|
||||
duplicate = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/collaborators",
|
||||
data=payload,
|
||||
content_type="application/json",
|
||||
)
|
||||
assert first.status_code == duplicate.status_code == 200
|
||||
assert first.json()["id"] == duplicate.json()["id"]
|
||||
assert TicketCollaborator.objects.filter(ticket=ticket).count() == 1
|
||||
assert TicketEvent.objects.filter(ticket=ticket, event_type="collaborator_added").count() == 1
|
||||
|
||||
listed = admin_client.get(f"/api/tickets/{ticket.id}/collaborators")
|
||||
assert listed.status_code == 200
|
||||
assert listed.json()[0]["user"]["display_name"] == "协作成员"
|
||||
|
||||
removed = admin_client.delete(
|
||||
f"/api/tickets/{ticket.id}/collaborators/{first.json()['id']}"
|
||||
)
|
||||
assert removed.status_code == 200
|
||||
assert TicketCollaborator.objects.filter(ticket=ticket).count() == 0
|
||||
|
||||
109
backend/apps/tickets/tests/test_ticket_domain_baseline.py
Normal file
109
backend/apps/tickets/tests/test_ticket_domain_baseline.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from apps.tickets.models import Ticket, TicketActorSource, TicketFieldEvidence
|
||||
from apps.tickets.services import update_ticket_fields
|
||||
|
||||
|
||||
def test_agent_update_does_not_overwrite_human_protected_field(db):
|
||||
user = get_user_model().objects.create_user(username="support-field-owner")
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607250001",
|
||||
title="原始标题",
|
||||
description="A",
|
||||
)
|
||||
ticket, _ = update_ticket_fields(
|
||||
ticket,
|
||||
{"title": "人工确认标题"},
|
||||
user,
|
||||
source=TicketActorSource.HUMAN,
|
||||
)
|
||||
|
||||
ticket, skipped = update_ticket_fields(
|
||||
ticket,
|
||||
{"title": "Agent 建议标题"},
|
||||
source=TicketActorSource.AGENT,
|
||||
agent_run_id=42,
|
||||
)
|
||||
|
||||
ticket.refresh_from_db()
|
||||
assert ticket.title == "人工确认标题"
|
||||
assert skipped == {"title": "protected_by_human"}
|
||||
latest = TicketFieldEvidence.objects.filter(ticket=ticket, field_name="title").first()
|
||||
assert latest.source == TicketActorSource.HUMAN
|
||||
assert latest.is_protected is True
|
||||
|
||||
|
||||
def test_agent_update_records_evidence_for_unprotected_field(db):
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607250002",
|
||||
title="原始标题",
|
||||
description="A",
|
||||
)
|
||||
|
||||
ticket, skipped = update_ticket_fields(
|
||||
ticket,
|
||||
{"related_system": "CRM"},
|
||||
source=TicketActorSource.AGENT,
|
||||
agent_run_id=99,
|
||||
confidence_by_field={"related_system": 0.93},
|
||||
evidence_by_field={"related_system": {"quote": "CRM 接口返回 500"}},
|
||||
)
|
||||
|
||||
assert skipped == {}
|
||||
evidence = TicketFieldEvidence.objects.get(ticket=ticket, field_name="related_system")
|
||||
assert evidence.value == "CRM"
|
||||
assert evidence.source == TicketActorSource.AGENT
|
||||
assert evidence.agent_run_id == 99
|
||||
assert float(evidence.confidence) == 0.93
|
||||
assert evidence.is_protected is False
|
||||
|
||||
|
||||
def test_archive_hides_ticket_from_default_list_and_restore_recovers_it(admin_client, db):
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607250003",
|
||||
title="待归档",
|
||||
description="A",
|
||||
)
|
||||
|
||||
archived = admin_client.post(f"/api/tickets/{ticket.id}/archive")
|
||||
default_list = admin_client.get("/api/tickets")
|
||||
archived_list = admin_client.get("/api/tickets?include_archived=true")
|
||||
|
||||
assert archived.status_code == 200
|
||||
assert archived.json()["archived_at"] is not None
|
||||
assert default_list.json()["total"] == 0
|
||||
assert archived_list.json()["total"] == 1
|
||||
|
||||
restored = admin_client.post(f"/api/tickets/{ticket.id}/restore")
|
||||
|
||||
assert restored.status_code == 200
|
||||
assert restored.json()["archived_at"] is None
|
||||
assert admin_client.get("/api/tickets").json()["total"] == 1
|
||||
|
||||
|
||||
def test_ticket_collaborators_can_be_added_listed_and_removed(admin_client, db):
|
||||
collaborator = get_user_model().objects.create_user(username="ticket-collaborator")
|
||||
ticket = Ticket.objects.create(
|
||||
ticket_no="TK202607250004",
|
||||
title="需要协作",
|
||||
description="A",
|
||||
)
|
||||
|
||||
created = admin_client.post(
|
||||
f"/api/tickets/{ticket.id}/collaborators",
|
||||
data={"user_id": collaborator.id, "role": "watcher"},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert created.status_code == 200
|
||||
assert created.json()["user"]["username"] == "ticket-collaborator"
|
||||
assert created.json()["role"] == "watcher"
|
||||
listed = admin_client.get(f"/api/tickets/{ticket.id}/collaborators")
|
||||
assert [item["id"] for item in listed.json()] == [created.json()["id"]]
|
||||
|
||||
removed = admin_client.delete(
|
||||
f"/api/tickets/{ticket.id}/collaborators/{created.json()['id']}"
|
||||
)
|
||||
|
||||
assert removed.status_code == 200
|
||||
assert admin_client.get(f"/api/tickets/{ticket.id}/collaborators").json() == []
|
||||
Reference in New Issue
Block a user