From 8136760b4b1bc3160844b855f1eaff8351de8fc7 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 17 Aug 2026 23:16:42 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E9=A2=84=E8=A7=88=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E5=9B=9E=E5=86=99=20cover=5Furl=20?= =?UTF-8?q?=E5=88=B0=20GenerationTask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:_finalize_render_success 更新 GenerationTask 时没有设置 cover_url 字段, 导致封面生成接口 fallback 全链路找不到封面,返回 400。 修复: - _finalize_render_success 新增 cover_candidates 参数 - 在 gen_task_repo.update 之前,取 cover_candidates[0] 的 image_url 写入 gen_task.cover_url - _render_with_unified 调用处传入 result.cover_candidates 测试:7 个单元测试覆盖各种边界情况 --- .../worker_app/tasks/edit_plan_generation.py | 19 +++ tests/unit/test_cover_url_finalize.py | 113 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 tests/unit/test_cover_url_finalize.py diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index cf0797b87..996093f6f 100644 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -98,6 +98,7 @@ def _finalize_render_success( output_path: Path, engine: str, thumbnail_url: str = "", + cover_candidates: list[dict] | None = None, ) -> dict: """渲染成功后的统一收尾:查重 + 更新状态 + 返回结果。""" # 创建 GeneratedVideo 记录 + 查重 @@ -163,6 +164,23 @@ def _finalize_render_success( clip_count=len(rendered_clip_ids), ) gen_task.completed_at = datetime.now(timezone.utc) + + # 回写封面 URL 到 GenerationTask,供封面生成接口读取 + if cover_candidates: + first_cover = ( + cover_candidates[0].get("image_url") + or cover_candidates[0].get("url") + or "" + ) + if first_cover: + gen_task.cover_url = first_cover + logger.info( + "预览渲染完成,回写 cover_url: plan_id=%s task_id=%s url=%s", + plan_id, + generation_task_id, + first_cover[:80], + ) + gen_task_repo.update(gen_task) logger.info( @@ -288,6 +306,7 @@ def _render_with_unified( output_path=output_path, engine="unified", thumbnail_url=thumbnail_url, + cover_candidates=result.cover_candidates, ) diff --git a/tests/unit/test_cover_url_finalize.py b/tests/unit/test_cover_url_finalize.py new file mode 100644 index 000000000..5bc28d13b --- /dev/null +++ b/tests/unit/test_cover_url_finalize.py @@ -0,0 +1,113 @@ +"""Tests for cover_url backfill to GenerationTask. + +Verifies _finalize_render_success correctly writes cover_url +from cover_candidates to gen_task.cover_url. +""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch + + +def _make_plan(): + plan = MagicMock() + plan.project_id = "proj-1" + plan.created_by_user_id = "user-1" + plan.config = {"batch_id": "batch-1", "mode": "edit_plan", "title": {"text": "test"}} + plan.mark_completed = MagicMock() + return plan + + +def _make_gen_task(): + gen_task = MagicMock() + gen_task.id = "task-1" + gen_task.status = MagicMock() + gen_task.status.value = "running" + return gen_task + + +def _call_finalize(cover_candidates=None, gen_task=None, plan=None): + from worker_app.tasks.edit_plan_generation import _finalize_render_success + + plan = plan or _make_plan() + gen_task = gen_task or _make_gen_task() + + plan_repo = MagicMock() + clip_repo = MagicMock() + gen_task_repo = MagicMock() + gen_task_repo.get.return_value = gen_task + db = MagicMock() + + with patch("worker_app.tasks.edit_plan_generation.create_video_record_and_dedup"): + result = _finalize_render_success( + plan=plan, + plan_repo=plan_repo, + clip_repo=clip_repo, + gen_task_repo=gen_task_repo, + db=db, + plan_id="plan-1", + output_url="https://oss.example.com/output.mp4", + storage_key="rendered/plan-1/task-1.mp4", + duration=10.0, + file_size=1024, + width=1280, + height=720, + rendered_clip_ids=["clip-1"], + failed_clip_ids=[], + generation_task_id="task-1", + output_path=Path("/tmp/output.mp4"), + engine="unified", + thumbnail_url="", + cover_candidates=cover_candidates, + ) + + return result, gen_task, gen_task_repo + + +class TestFinalizeCoverUrl: + + def test_cover_url_set_from_image_url(self): + """cover_candidates with image_url should set gen_task.cover_url""" + candidates = [ + {"image_url": "https://oss.example.com/cover1.jpg", "frame_time": 1.5}, + {"image_url": "https://oss.example.com/cover2.jpg", "frame_time": 3.0}, + ] + _, gen_task, gen_task_repo = _call_finalize(cover_candidates=candidates) + assert gen_task.cover_url == "https://oss.example.com/cover1.jpg" + gen_task_repo.update.assert_called() + + def test_cover_url_fallback_to_url_key(self): + """Should fallback to 'url' key when 'image_url' is absent""" + candidates = [{"url": "https://oss.example.com/cover_url_key.jpg"}] + _, gen_task, _ = _call_finalize(cover_candidates=candidates) + assert gen_task.cover_url == "https://oss.example.com/cover_url_key.jpg" + + def test_cover_url_not_set_when_empty_list(self): + """Empty cover_candidates should not set cover_url""" + _, gen_task, _ = _call_finalize(cover_candidates=[]) + assert not getattr(gen_task, "cover_url", None) + + def test_cover_url_not_set_when_none(self): + """None cover_candidates should not set cover_url""" + _, gen_task, _ = _call_finalize(cover_candidates=None) + assert not getattr(gen_task, "cover_url", None) + + def test_cover_url_not_set_when_url_empty(self): + """Empty URL strings in candidates should not set cover_url""" + candidates = [{"image_url": "", "url": ""}] + _, gen_task, _ = _call_finalize(cover_candidates=candidates) + assert not getattr(gen_task, "cover_url", None) + + def test_no_generation_task_no_crash(self): + """Should not crash when gen_task is None""" + candidates = [{"image_url": "https://oss.example.com/cover.jpg"}] + gen_task_repo = MagicMock() + gen_task_repo.get.return_value = None + _, _, _ = _call_finalize(cover_candidates=candidates) + + def test_image_url_priority_over_url(self): + """image_url should take priority over url key""" + candidates = [{"image_url": "https://a.jpg", "url": "https://b.jpg"}] + _, gen_task, _ = _call_finalize(cover_candidates=candidates) + assert gen_task.cover_url == "https://a.jpg" -- 2.54.0 From f97e6e3a4fd4f1708b046c653fea5743fa698523 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 17 Aug 2026 15:20:41 +0000 Subject: [PATCH 2/5] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/worker/worker_app/tasks/edit_plan_generation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index 996093f6f..69ce7311d 100644 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -167,11 +167,7 @@ def _finalize_render_success( # 回写封面 URL 到 GenerationTask,供封面生成接口读取 if cover_candidates: - first_cover = ( - cover_candidates[0].get("image_url") - or cover_candidates[0].get("url") - or "" - ) + first_cover = cover_candidates[0].get("image_url") or cover_candidates[0].get("url") or "" if first_cover: gen_task.cover_url = first_cover logger.info( -- 2.54.0 From 44efd776e94ebc5589b7b4e882378206504bf2d0 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 17 Aug 2026 23:38:07 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(test):=20=E4=BF=AE=E5=A4=8D=20MagicMock?= =?UTF-8?q?=20=E5=B1=9E=E6=80=A7=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E6=96=AD=E8=A8=80=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 FakeGenTask 替代 MagicMock,通过追踪 _assigned 字典 准确判断 cover_url 是否被显式设置。 --- tests/unit/test_cover_url_finalize.py | 42 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/tests/unit/test_cover_url_finalize.py b/tests/unit/test_cover_url_finalize.py index 5bc28d13b..079554e36 100644 --- a/tests/unit/test_cover_url_finalize.py +++ b/tests/unit/test_cover_url_finalize.py @@ -6,9 +6,32 @@ from cover_candidates to gen_task.cover_url. from __future__ import annotations +import sys from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + +# Add worker app to sys.path +_WORKER_ROOT = Path(__file__).resolve().parents[2] / "apps" / "worker" +if str(_WORKER_ROOT) not in sys.path: + sys.path.insert(0, str(_WORKER_ROOT)) + + +class FakeGenTask: + """Simple stand-in for GenerationTask that tracks attribute assignment.""" + + def __init__(self): + self.id = "task-1" + self.status = MagicMock() + self.status.value = "running" + self._assigned = {} + + def __setattr__(self, name, value): + if not name.startswith("_"): + self._assigned[name] = value + super().__setattr__(name, value) + def _make_plan(): plan = MagicMock() @@ -19,19 +42,11 @@ def _make_plan(): return plan -def _make_gen_task(): - gen_task = MagicMock() - gen_task.id = "task-1" - gen_task.status = MagicMock() - gen_task.status.value = "running" - return gen_task - - def _call_finalize(cover_candidates=None, gen_task=None, plan=None): from worker_app.tasks.edit_plan_generation import _finalize_render_success plan = plan or _make_plan() - gen_task = gen_task or _make_gen_task() + gen_task = gen_task or FakeGenTask() plan_repo = MagicMock() clip_repo = MagicMock() @@ -86,25 +101,26 @@ class TestFinalizeCoverUrl: def test_cover_url_not_set_when_empty_list(self): """Empty cover_candidates should not set cover_url""" _, gen_task, _ = _call_finalize(cover_candidates=[]) - assert not getattr(gen_task, "cover_url", None) + assert "cover_url" not in gen_task._assigned def test_cover_url_not_set_when_none(self): """None cover_candidates should not set cover_url""" _, gen_task, _ = _call_finalize(cover_candidates=None) - assert not getattr(gen_task, "cover_url", None) + assert "cover_url" not in gen_task._assigned def test_cover_url_not_set_when_url_empty(self): """Empty URL strings in candidates should not set cover_url""" candidates = [{"image_url": "", "url": ""}] _, gen_task, _ = _call_finalize(cover_candidates=candidates) - assert not getattr(gen_task, "cover_url", None) + assert "cover_url" not in gen_task._assigned def test_no_generation_task_no_crash(self): """Should not crash when gen_task is None""" candidates = [{"image_url": "https://oss.example.com/cover.jpg"}] gen_task_repo = MagicMock() gen_task_repo.get.return_value = None - _, _, _ = _call_finalize(cover_candidates=candidates) + result, _, _ = _call_finalize(cover_candidates=candidates) + assert result["status"] == "completed" def test_image_url_priority_over_url(self): """image_url should take priority over url key""" -- 2.54.0 From e52b63201a5502b316cee6a84515f1849e2ff865 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 17 Aug 2026 23:51:58 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(test):=20=E4=BF=AE=E5=A4=8D=20FakeGenTa?= =?UTF-8?q?sk=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=BA=E5=BA=8F=E5=8F=8A=20a?= =?UTF-8?q?ppend=5Flog=20=E7=BC=BA=E5=A4=B1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_cover_url_finalize.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_cover_url_finalize.py b/tests/unit/test_cover_url_finalize.py index 079554e36..48417a196 100644 --- a/tests/unit/test_cover_url_finalize.py +++ b/tests/unit/test_cover_url_finalize.py @@ -22,15 +22,18 @@ class FakeGenTask: """Simple stand-in for GenerationTask that tracks attribute assignment.""" def __init__(self): + object.__setattr__(self, '_assigned', {}) self.id = "task-1" self.status = MagicMock() self.status.value = "running" - self._assigned = {} def __setattr__(self, name, value): if not name.startswith("_"): self._assigned[name] = value - super().__setattr__(name, value) + object.__setattr__(self, name, value) + + def append_log(self, **kwargs): + pass def _make_plan(): -- 2.54.0 From de9c34f481e86ec2794cc3f6bd62afd6cd08621f Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 17 Aug 2026 15:57:00 +0000 Subject: [PATCH 5/5] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_cover_url_finalize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_cover_url_finalize.py b/tests/unit/test_cover_url_finalize.py index 48417a196..d47513e62 100644 --- a/tests/unit/test_cover_url_finalize.py +++ b/tests/unit/test_cover_url_finalize.py @@ -22,7 +22,7 @@ class FakeGenTask: """Simple stand-in for GenerationTask that tracks attribute assignment.""" def __init__(self): - object.__setattr__(self, '_assigned', {}) + object.__setattr__(self, "_assigned", {}) self.id = "task-1" self.status = MagicMock() self.status.value = "running" -- 2.54.0