feat: consolidate ticket domain baseline

This commit is contained in:
hailin
2026-07-27 18:57:51 +08:00
parent 5015e4407d
commit 96452934a3
25 changed files with 4236 additions and 218 deletions

View File

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

View 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() == []