feat: complete multimodal ticket intake
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
@@ -95,6 +96,30 @@ def test_feature_requires_published_ticket_agent(admin_client, db):
|
||||
assert response.json()["vision_available"] is False
|
||||
|
||||
|
||||
def test_availability_uses_effective_step_vision_model(admin_client, db):
|
||||
agent = create_ticket_agent()
|
||||
version = agent.active_version
|
||||
provider = version.model_deployment.provider
|
||||
provider.image_egress_approved = True
|
||||
provider.save(update_fields=["image_egress_approved"])
|
||||
vision = ModelDeployment.objects.create(
|
||||
provider=provider,
|
||||
display_name="步骤视觉模型",
|
||||
model_name="ticket-vision-model",
|
||||
capability="vision_chat",
|
||||
)
|
||||
binding = version.steps.get()
|
||||
binding.model_deployment = vision
|
||||
binding.save(update_fields=["model_deployment"])
|
||||
|
||||
response = admin_client.get("/api/ticket-drafts/availability")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["available"] is True
|
||||
assert response.json()["vision_available"] is True
|
||||
assert response.json()["vision_reason"] == ""
|
||||
|
||||
|
||||
def test_revision_conflict_and_manual_edit(admin_client, db):
|
||||
create_ticket_agent()
|
||||
created = admin_client.post(
|
||||
@@ -145,9 +170,16 @@ def test_confirm_is_idempotent_and_records_accepted(admin_client, category):
|
||||
assert first.status_code == 200
|
||||
assert second.status_code == 200
|
||||
assert first.json()["confirmed_ticket"]["id"] == second.json()["confirmed_ticket"]["id"]
|
||||
assert first.json()["created_ticket"]["id"] == first.json()["confirmed_ticket"]["id"]
|
||||
assert first.json()["decision"] == "accepted"
|
||||
assert Ticket.objects.count() == 1
|
||||
assert Ticket.objects.get().ai_solution == ""
|
||||
assert not hasattr(Ticket.objects.get(), "ai_solution")
|
||||
ticket_id = first.json()["created_ticket"]["id"]
|
||||
trace = admin_client.get(f"/api/tickets/{ticket_id}/intake-trace")
|
||||
assert trace.status_code == 200
|
||||
assert trace.json()["available"] is True
|
||||
assert trace.json()["status"] == "ticket_created"
|
||||
assert admin_client.get(f"/api/tickets/{ticket_id}/attachments").json() == []
|
||||
|
||||
|
||||
def test_confirm_records_modified_and_reject_records_rejected(admin_client, category):
|
||||
@@ -201,13 +233,14 @@ def test_image_upload_validates_content_and_never_audits_source_text(admin_clien
|
||||
monkeypatch.setattr("apps.tickets.draft_api.put_object", lambda *args: None)
|
||||
output = io.BytesIO()
|
||||
Image.new("RGB", (8, 8), color="red").save(output, format="PNG")
|
||||
upload = SimpleUploadedFile("proof.png", output.getvalue(), content_type="text/plain")
|
||||
upload = SimpleUploadedFile("proof.png", output.getvalue(), content_type="image/png")
|
||||
|
||||
response = admin_client.post(
|
||||
f"/api/ticket-drafts/{created['public_id']}/images", {"file": upload}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["images"][0]["mime_type"] == "image/png"
|
||||
assert response.json()["materials"][0]["scan_status"] == "clean"
|
||||
snapshots = list(
|
||||
OperationAuditLog.objects.filter(target_id=created["public_id"]).values_list(
|
||||
"request_snapshot", flat=True
|
||||
@@ -224,12 +257,14 @@ def test_text_model_rejects_image_analysis(admin_client, db):
|
||||
content_type="application/json",
|
||||
).json()
|
||||
draft = TicketDraft.objects.get(public_id=created["public_id"])
|
||||
draft.images.create(
|
||||
draft.materials.create(
|
||||
file_name="proof.png",
|
||||
mime_type="image/png",
|
||||
file_size=10,
|
||||
sha256="0" * 64,
|
||||
storage_path="ticket-drafts/proof.png",
|
||||
scan_status="clean",
|
||||
parse_status="not_applicable",
|
||||
)
|
||||
response = admin_client.post(
|
||||
f"/api/ticket-drafts/{created['public_id']}/analyze",
|
||||
@@ -240,6 +275,38 @@ def test_text_model_rejects_image_analysis(admin_client, db):
|
||||
assert "不支持图片" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_vision_intake_can_start_with_image_only(admin_client, db, monkeypatch):
|
||||
create_ticket_agent(capability="vision_chat", approved=True)
|
||||
created = admin_client.post(
|
||||
"/api/ticket-drafts",
|
||||
data={"source_text": ""},
|
||||
content_type="application/json",
|
||||
).json()
|
||||
draft = TicketDraft.objects.get(public_id=created["public_id"])
|
||||
draft.materials.create(
|
||||
file_name="proof.png",
|
||||
mime_type="image/png",
|
||||
file_size=10,
|
||||
sha256="0" * 64,
|
||||
storage_path="ticket-drafts/proof.png",
|
||||
scan_status="clean",
|
||||
parse_status="not_applicable",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"apps.tickets.draft_api.execute_ticket_draft_analysis.delay",
|
||||
lambda *_args: SimpleNamespace(id="task-1"),
|
||||
)
|
||||
|
||||
response = admin_client.post(
|
||||
f"/api/ticket-drafts/{created['public_id']}/analyze",
|
||||
data={"expected_revision": created["revision"]},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "analyzing"
|
||||
|
||||
|
||||
def test_cleanup_deletes_unconfirmed_details_but_keeps_confirmed_draft(
|
||||
admin_client, db, monkeypatch
|
||||
):
|
||||
@@ -251,7 +318,7 @@ def test_cleanup_deletes_unconfirmed_details_but_keeps_confirmed_draft(
|
||||
source_text="过期敏感原文",
|
||||
expires_at=timezone.now(),
|
||||
)
|
||||
expired.images.create(
|
||||
expired.materials.create(
|
||||
file_name="proof.png",
|
||||
mime_type="image/png",
|
||||
file_size=10,
|
||||
|
||||
Reference in New Issue
Block a user