fix(file-summary): 同步压缩包工作流状态与结果刷新
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"] == "未解出任何可扫描文件"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user