fix: 保持产物收尾幂等并保护批量成品完整性

This commit is contained in:
2026-09-11 16:12:32 +08:00
parent f3faad0391
commit 3a612919f7
8 changed files with 293 additions and 43 deletions
+26 -10
View File
@@ -682,13 +682,16 @@ def test_batch_worker_pending_leftover_never_marks_completed(tmp_path, monkeypat
monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _CrashScheduler)
# 放开假调度器的崩溃:第二次调用时不再删 run,让真实调度器跑通。
class _RecoveringScheduler:
"""第二轮:不再崩溃,把 run 置 COMPLETED由引擎收尾放产物)"""
"""第二轮:真实执行工作流并产生成品,由引擎收尾放"""
def __init__(self, db: Database, work_dir: Path) -> None:
self.db = db
def execute_run(self, run_id: str) -> None:
self.db.update_run(run_id, status="COMPLETED", updated_at=_now_iso())
from wov_app.scheduler import WorkflowScheduler
video = self.db.list_batch_videos(job["id"])[0]
WorkflowScheduler(self.db, Path(video["work_dir"])).execute_run(run_id)
monkeypatch.setattr("wov_app.batch.WorkflowScheduler", _RecoveringScheduler)
worker._process_job(job)
@@ -701,7 +704,7 @@ def test_batch_worker_already_completed_runs_finalized(tmp_path) -> None:
"""run 已完成但视频未标记(收尾前中断):直接放置产物、清理并标记完成。
覆盖 _place_products 各分支:产物齐全(复制)、产物记录存在但文件丢失
跳过)、无产物记录(跳过)——完成后均清理工作空间与 run 记录。
报错)、无产物记录(报错)——只有成品齐全才清理工作空间与 run 记录。
"""
db = _db(tmp_path)
_seed_echo_workflow(db)
@@ -714,6 +717,7 @@ def test_batch_worker_already_completed_runs_finalized(tmp_path) -> None:
"""创建 COMPLETED run;可选产物文件与产物记录。"""
_create_run(db, run_id, folder, status="COMPLETED", name=name)
work_dir = Path(videos[name]["work_dir"])
work_dir.mkdir(parents=True)
if with_artifact:
path = work_dir / "runs" / run_id / "steps" / "step" / f"{name}.result.txt"
db.create_artifact(
@@ -739,20 +743,24 @@ def test_batch_worker_already_completed_runs_finalized(tmp_path) -> None:
BatchWorker(db, interval_seconds=0.05)._process_job(db.get_batch_job(job["id"]))
videos = {Path(v["video_path"]).stem: v for v in db.list_batch_videos(job["id"])}
assert all(videos[name]["status"] == "COMPLETED" for name in ("a", "b", "c"))
assert videos["a"]["status"] == "COMPLETED"
assert all(videos[name]["status"] == "FAILED" for name in ("b", "c"))
# a 的产物复制到视频旁;b/c 无产物可复制。
product = _find_product(folder, "a", "result")
assert product.read_text(encoding="utf-8") == "产物 a"
assert not list(folder.glob("b.result.*")) and not list(folder.glob("c.result.*"))
# 三个视频的工作空间 run 记录都被清理
for name in ("a", "b", "c"):
assert not Path(videos[name]["work_dir"]).exists()
assert db.get_run(f"run_done_{name}") is None
assert db.list_runs() == []
# a 清理完成;b/c 必须保留工作空间、关联 run 与错误供修复后重试
assert not Path(videos["a"]["work_dir"]).exists()
assert db.get_run("run_done_a") is None
for name in ("b", "c"):
assert Path(videos[name]["work_dir"]).exists()
assert db.get_run(f"run_done_{name}") is not None
assert videos[name]["run_id"] == f"run_done_{name}"
assert "result" in videos[name]["error"]
def test_batch_worker_place_products_branches(tmp_path) -> None:
"""_place_products:无产物记录/文件丢失跳过;.srt/.ass 按库内约定命名覆盖;
"""_place_products:无产物记录/文件丢失报错;.srt/.ass 按库内约定命名覆盖;
其他扩展名保留原文件名复制。"""
db = _db(tmp_path)
_seed_echo_workflow(db)
@@ -805,6 +813,14 @@ def test_batch_worker_place_products_branches(tmp_path) -> None:
{"run_id": run_id, "node_id": "step", "name": "alias_txt", "uri": str(src_txt), "mime_type": "text/plain", "size": src_txt.stat().st_size}
)
# 所有必需成品预检通过前不开始覆盖,缺失原因中包含具体别名。
with pytest.raises(ValueError, match="alias_none"):
worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition))
del definition["final_outputs"]["alias_none"]
with pytest.raises(ValueError, match="alias_lost"):
worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition))
assert target_srt.read_text(encoding="utf-8") == "旧字幕内容"
del definition["final_outputs"]["alias_lost"]
placed = worker._place_products(run_id, video, WorkflowDefinition.from_dict(definition))
assert placed == ["a.CN.srt", "a.CN_dual_eye.ass", "a.other.20260902120000.txt"]
# srt/ass 被改名为标准名且覆盖旧文件;txt 保留原名。