fix(file-summary): 同步压缩包工作流状态与结果刷新

This commit is contained in:
2026-06-06 19:45:49 +08:00
parent daa0642142
commit 7e561ea213
12 changed files with 560 additions and 32 deletions

View File

@@ -25,6 +25,8 @@ def test_workspace_renders_summary_panel(client, django_user_model):
assert 'id="uploadDropzone"' in content
assert 'id="workflowCardList"' in content
assert 'data-conversation-id="' in content
assert 'data-message-id="' in content
assert 'data-message-url-template="' in content
assert 'class="message-content markdown-content"' in content
assert 'class="message-raw"' in content
assert "自动汇总文件目录与页数" in content
@@ -52,3 +54,37 @@ def test_frontend_updates_sidebar_conversation_by_stable_id():
assert "data-conversation-id" in script
assert "setAttribute(\"data-conversation-id\"" in script
assert ".history-item[data-conversation-id=" in script
def test_frontend_refreshes_generated_workflow_messages():
script = open("static/js/app.js", encoding="utf-8").read()
assert "refreshConversationMessages" in script
assert "latestMessageId" in script
assert "data-message-url-template" in script
def test_frontend_can_replace_partial_stream_content():
script = open("static/js/app.js", encoding="utf-8").read()
assert 'eventName === "replace"' in script
assert "assistantText = payload.content" in script
def test_frontend_enter_sends_and_ctrl_enter_inserts_newline():
script = open("static/js/app.js", encoding="utf-8").read()
assert "bindPromptKeyboardShortcuts" in script
assert "event.key === \"Enter\"" in script
assert "event.ctrlKey" in script
assert "composer.requestSubmit()" in script
def test_frontend_renders_workflow_error_messages():
script = open("static/js/app.js", encoding="utf-8").read()
css = open("static/css/login.css", encoding="utf-8").read()
assert "payload.batch.error_message" in script
assert "workflow-error" in script
assert "node.message" in script
assert ".workflow-error" in css

View File

@@ -2,7 +2,14 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
import pytest
from review_agent.models import Conversation, ExportedSummaryFile, FileAttachment, FileSummaryBatch
from review_agent.models import (
Conversation,
ExportedSummaryFile,
FileAttachment,
FileSummaryBatch,
Message,
WorkflowNodeRun,
)
pytestmark = pytest.mark.django_db
@@ -99,3 +106,68 @@ def test_export_download_requires_batch_owner(client, tmp_path, django_user_mode
assert "attachment" in allowed["Content-Disposition"]
assert "summary.md" in allowed["Content-Disposition"]
assert allowed["Content-Type"].startswith("text/markdown")
def test_conversation_messages_returns_incremental_messages(client, django_user_model):
owner = django_user_model.objects.create_user(username="owner", password="pass")
other = django_user_model.objects.create_user(username="other", password="pass")
conversation = Conversation.objects.create(user=owner, title="会话")
first = Message.objects.create(
conversation=conversation,
role=Message.Role.USER,
content="用户消息",
)
second = Message.objects.create(
conversation=conversation,
role=Message.Role.ASSISTANT,
content="报告消息",
)
client.force_login(other)
denied = client.get(reverse("review_agent_conversation_messages", args=[conversation.pk]))
assert denied.status_code == 404
client.force_login(owner)
response = client.get(
f"{reverse('review_agent_conversation_messages', args=[conversation.pk])}?after={first.pk}"
)
assert response.status_code == 200
payload = response.json()
assert payload["latest_message_id"] == second.pk
assert payload["messages"] == [
{
"id": second.pk,
"role": Message.Role.ASSISTANT,
"content": "报告消息",
"created_at": second.created_at.isoformat(),
}
]
def test_batch_status_exposes_batch_and_node_errors(client, django_user_model):
user = django_user_model.objects.create_user(username="owner", password="pass")
conversation = Conversation.objects.create(user=user, title="会话")
batch = FileSummaryBatch.objects.create(
conversation=conversation,
user=user,
batch_no="FS-ERR",
status=FileSummaryBatch.Status.FAILED,
error_message="压缩包解压失败",
)
WorkflowNodeRun.objects.create(
batch=batch,
node_code="extract",
node_name="压缩包解压",
status=WorkflowNodeRun.Status.FAILED,
progress=10,
message="未解出任何可扫描文件",
)
client.force_login(user)
response = client.get(reverse("file_summary_batch_status", args=[batch.pk]))
assert response.status_code == 200
payload = response.json()
assert payload["batch"]["error_message"] == "压缩包解压失败"
assert payload["nodes"][0]["message"] == "未解出任何可扫描文件"

View File

@@ -1,5 +1,8 @@
import pytest
from pathlib import Path
from zipfile import ZipFile
from review_agent.file_summary.services import archive as archive_service
from review_agent.file_summary.workflow import create_file_summary_batch, start_file_summary_workflow
from review_agent.skill_router import SkillRoute
from review_agent.models import (
@@ -43,6 +46,7 @@ def test_create_batch_binds_active_attachments_and_initializes_nodes(django_user
assert FileSummaryBatchAttachment.objects.get(batch=batch).attachment == active
active.refresh_from_db()
assert active.upload_status == FileAttachment.UploadStatus.BOUND
assert batch.work_dir
assert WorkflowNodeRun.objects.filter(batch=batch).count() >= 6
assert WorkflowEvent.objects.filter(batch=batch, event_type="workflow_created").exists()
@@ -67,6 +71,88 @@ def test_start_file_summary_workflow_runs_synchronously_for_tests(django_user_mo
assert WorkflowEvent.objects.filter(batch=batch, event_type="workflow_completed").exists()
def test_workflow_extracts_archive_and_scans_extracted_files(settings, tmp_path, django_user_model):
settings.MEDIA_ROOT = tmp_path
user = django_user_model.objects.create_user(username="owner", password="pass")
conversation = Conversation.objects.create(user=user, title="会话")
archive_path = tmp_path / "upload.zip"
with ZipFile(archive_path, "w") as archive:
archive.writestr("folder/a.pdf", b"%PDF-1.4\n%%EOF")
FileAttachment.objects.create(
conversation=conversation,
user=user,
original_name="upload.zip",
storage_path=str(archive_path),
file_size=archive_path.stat().st_size,
)
batch = create_file_summary_batch(conversation=conversation, user=user)
start_file_summary_workflow(batch, async_run=False)
batch.refresh_from_db()
assert batch.total_files == 1
assert batch.items.get().file_name == "a.pdf"
assert not batch.items.filter(file_type="zip").exists()
def test_workflow_marks_archive_extract_failure_visible(settings, tmp_path, django_user_model):
settings.MEDIA_ROOT = tmp_path
user = django_user_model.objects.create_user(username="owner", password="pass")
conversation = Conversation.objects.create(user=user, title="会话")
archive_path = tmp_path / "empty.zip"
with ZipFile(archive_path, "w"):
pass
FileAttachment.objects.create(
conversation=conversation,
user=user,
original_name="empty.zip",
storage_path=str(archive_path),
file_size=archive_path.stat().st_size,
)
batch = create_file_summary_batch(conversation=conversation, user=user)
start_file_summary_workflow(batch, async_run=False)
batch.refresh_from_db()
extract_node = batch.node_runs.get(node_code="extract")
assert batch.status == FileSummaryBatch.Status.FAILED
assert "未解出任何可扫描文件" in batch.error_message
assert extract_node.status == WorkflowNodeRun.Status.FAILED
assert "未解出任何可扫描文件" in extract_node.message
failed_event = WorkflowEvent.objects.filter(
batch=batch,
event_type="node_progress",
payload__status=WorkflowNodeRun.Status.FAILED,
).latest("id")
assert "未解出任何可扫描文件" in failed_event.payload["message"]
def test_rar_extract_uses_python_libarchive_before_7z(monkeypatch, tmp_path):
archive_path = tmp_path / "sample.rar"
archive_path.write_bytes(b"rar")
target_dir = tmp_path / "out"
calls = []
def fake_libarchive_extract(path: Path, target: Path):
calls.append(("libarchive", path, target))
extracted = target / "a.docx"
extracted.parent.mkdir(parents=True, exist_ok=True)
extracted.write_bytes(b"doc")
return [extracted]
def fake_7z_extract(path: Path, target: Path):
calls.append(("7z", path, target))
return []
monkeypatch.setattr(archive_service, "_extract_rar_with_libarchive", fake_libarchive_extract)
monkeypatch.setattr(archive_service, "_extract_rar_with_7z", fake_7z_extract)
extracted = archive_service.extract_archive(archive_path, target_dir)
assert [path.name for path in extracted] == ["a.docx"]
assert calls == [("libarchive", archive_path, target_dir)]
def test_stream_message_returns_workflow_meta_when_triggered(settings, django_user_model):
settings.FILE_SUMMARY_ASYNC = False
user = django_user_model.objects.create_user(username="owner", password="pass")
@@ -142,7 +228,7 @@ def test_stream_message_reads_active_attachment_when_requested(settings, tmp_pat
assert "workflow_started" not in joined
def test_stream_message_returns_error_event_when_unexpected_stream_error(monkeypatch, django_user_model):
def test_stream_message_falls_back_to_non_stream_reply_when_stream_breaks(monkeypatch, django_user_model):
user = django_user_model.objects.create_user(username="owner", password="pass")
conversation = Conversation.objects.create(user=user, title="会话")
@@ -151,14 +237,17 @@ def test_stream_message_returns_error_event_when_unexpected_stream_error(monkeyp
raise RuntimeError("provider connection reset")
monkeypatch.setattr("review_agent.services.stream_reply", broken_stream_reply)
monkeypatch.setattr("review_agent.services.generate_reply", lambda conversation, content: "非流式完整回复")
frames = list(stream_message(conversation, "普通问题"))
joined = "".join(frames)
assert "已生成部分内容" in joined
assert "回复生成中断" in joined
assert "replace" in joined
assert "非流式完整回复" in joined
assert "done" in joined
assert Message.objects.filter(conversation=conversation, role=Message.Role.ASSISTANT).exists()
assistant_message = Message.objects.get(conversation=conversation, role=Message.Role.ASSISTANT)
assert assistant_message.content == "非流式完整回复"
def test_stream_message_uses_llm_router_for_attachment_reader(