fix: 保持产物收尾幂等并保护批量成品完整性
This commit is contained in:
+26
-10
@@ -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 保留原名。
|
||||
|
||||
Reference in New Issue
Block a user