feat: add favorite task templates and optimize evidence exports

This commit is contained in:
chenmingxuan committed 2026-10-18 19:44:51 +08:00
1 parent e1ea099592
commit a4a5430922
46 files changed
+1714 -188

No files matched your search

+76 -3
View File
@@ -158,6 +158,7 @@ def test_completed_batch_reuses_existing_archive(client, isolated, monkeypatch):
path = reports.EXPORT_ROOT / "batches" / batch_id / "批次报告.zip"
path.parent.mkdir(parents=True)
path.write_bytes(b"existing archive")
reports.remember(path, reports.BATCH_ARCHIVE_SIGNATURE)
exporter = Mock(side_effect=AssertionError("archive must not be rebuilt"))
monkeypatch.setattr(reports, "export_batch", exporter)
base = f"/api/automation/batches/{batch_id}/archive"
@@ -166,6 +167,21 @@ def test_completed_batch_reuses_existing_archive(client, isolated, monkeypatch):
exporter.assert_not_called()
def test_legacy_batch_archive_is_rebuilt_with_current_recording_layout(client, isolated):
batch = service.create_batch(payload())
batch_id = batch["id"]
isolated.put("batch", batch_id, {**batch, "status": "completed", "report_ready": True})
path = reports.EXPORT_ROOT / "batches" / batch_id / "批次报告.zip"
path.parent.mkdir(parents=True)
path.write_bytes(b"legacy archive")
base = f"/api/automation/batches/{batch_id}/archive"
assert client.get(base + "/status").json()["status"] == "idle"
response = client.get(base)
assert response.status_code == 200
assert zipfile.is_zipfile(BytesIO(response.content))
assert client.get(base + "/status").json()["status"] == "ready"
def test_preview_does_not_enqueue(client, isolated):
response = client.post(
"/api/automation/import/preview",
@@ -1021,7 +1037,7 @@ def test_task_material_zip_contains_ordered_evidence_and_reports(
assert files.index("1.1/步骤截图/0001_步骤0001_操作前.png") < files.index(
"1.1/步骤截图/0002_步骤0002_操作后.png"
)
assert archive.read("1.1/任务录屏/0001_recording.mp4") == b"recording.mp4"
assert archive.read("1.1/任务录屏/安卓/0001_recording.mp4") == b"recording.mp4"
manifest = json.loads(archive.read("1.1/文件清单.json"))
assert manifest["session_id"] == job["session_id"]
assert manifest["artifacts"] == ["安卓截图.png"]
@@ -1042,6 +1058,63 @@ def test_batch_zip_contains_each_tasks_android_artifacts(isolated):
assert {archive.read(name).decode() for name in names} == {"任务0", "任务1"}
def test_all_material_cache_reuses_reports_and_invalidates_on_review_or_new_files(
isolated, monkeypatch
):
job = isolated.jobs(service.create_batch(payload())["id"])[0]
job["status"] = "completed"
isolated.save_job(job)
word = Mock(wraps=reports.word_report)
monkeypatch.setattr(reports, "word_report", word)
monkeypatch.setattr(
reports,
"diagnostics_json",
lambda sid: json.dumps({"requests": [], "collected_at": time.time()}),
)
first = reports.export_session_media(job["session_id"], "all")
first_bytes = first.read_bytes()
reports.export_session_media(job["session_id"], "all")
assert word.call_count == 1
assert first.read_bytes() == first_bytes
isolated.save_case_review(job["session_id"], "passed", "人工核对")
reports.export_session_media(job["session_id"], "all")
assert word.call_count == 2
folder = reports.TRACES_PATH / job["session_id"] / "artifacts" / "android"
folder.mkdir(parents=True)
(folder / "新资料.txt").write_text("new evidence", encoding="utf-8")
reports.export_session_media(job["session_id"], "all")
with zipfile.ZipFile(first) as archive:
assert any(name.endswith("任务文件/安卓/新资料.txt") for name in archive.namelist())
assert not json.loads(
archive.read(next(n for n in archive.namelist() if n.endswith("文件清单.json")))
)["snapshot"]
# File-only changes rebuild the ZIP while keeping unchanged documents.
assert word.call_count == 2
def test_different_tasks_export_concurrently_and_running_exports_are_snapshots(
isolated, monkeypatch
):
from concurrent.futures import ThreadPoolExecutor
jobs = isolated.jobs(service.create_batch(payload())["id"])
barrier = threading.Barrier(2)
def generate(report, path):
barrier.wait(timeout=5)
path.write_bytes(b"generated report")
monkeypatch.setattr(reports, "word_report", generate)
with ThreadPoolExecutor(max_workers=2) as pool:
exports = [pool.submit(reports.export_session, j["session_id"], ["docx"]) for j in jobs]
assert all(result.result(timeout=10)[0].is_file() for result in exports)
count = Mock(side_effect=lambda report, path: path.write_bytes(b"snapshot"))
monkeypatch.setattr(reports, "word_report", count)
for _ in range(2):
reports.export_session(jobs[0]["session_id"], ["docx"])
assert count.call_count == 2
def test_android_artifact_export_does_not_follow_other_session_links(isolated):
root = reports.TRACES_PATH / "session" / "artifacts"
root.mkdir(parents=True)
@@ -1078,8 +1151,8 @@ def test_video_export_accepts_finalized_worker_trace_and_rejects_external_path(
with zipfile.ZipFile(archive_path) as archive:
names = archive.namelist()
assert names == [
"任务录屏/0001_web_123_" + session_id[:8] + "_PASS_2026-09-28_recording.mp4",
"任务录屏/0002_resume_0001_recording.mp4",
"任务录屏/安卓/0001_web_123_" + session_id[:8] + "_PASS_2026-09-28_recording.mp4",
"任务录屏/安卓/0002_resume_0001_recording.mp4",
"文件清单.json",
]
assert archive.read(names[0]) == b"first segment"