From 953911683fb41c3cd6573ec9f7af0ff8d4550307 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 22 Aug 2026 21:25:49 +0800 Subject: [PATCH] test(cover): add unit tests for cover_candidates fallback (step D) Add 3 tests for the plan.config.cover_candidates fallback path: - test_cover_url_found_via_cover_candidates_image_url: valid image_url key - test_cover_url_found_via_cover_candidates_url_key: valid url key fallback - test_cover_candidates_skips_non_dict_first_element: non-dict safety This brings diff coverage above 40% threshold for generation_cover.py. --- tests/unit/test_generation_cover.py | 148 ++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/tests/unit/test_generation_cover.py b/tests/unit/test_generation_cover.py index a7163087f..44e98a75b 100644 --- a/tests/unit/test_generation_cover.py +++ b/tests/unit/test_generation_cover.py @@ -318,6 +318,154 @@ class TestUnifiedCoverPipelineEndpoint: template_id="template-y", ) + def test_cover_url_found_via_cover_candidates_image_url(self): + """步骤D:plan.config.cover_candidates 有 image_url 时,直接使用第一个候选封面。""" + from unittest.mock import MagicMock, patch + + from app.api.routes.generation_cover import GenerateCoverRequest + + mock_plan = MagicMock() + # 步骤A/B/C 都找不到,进入步骤D + mock_plan.config = { + "cover_candidates": [ + {"image_url": "https://oss.example.com/candidates/cover-1.jpg", "score": 0.95}, + {"image_url": "https://oss.example.com/candidates/cover-2.jpg", "score": 0.80}, + ], + } + + mock_plan_svc = MagicMock() + mock_plan_svc.get_plan_or_raise.return_value = mock_plan + mock_template_svc = MagicMock() + mock_db = MagicMock() + + body = GenerateCoverRequest(cover_type="ai_frame") + + with ( + patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls, + patch("app.api.routes.generation_cover.normalize_plan_config") as mock_normalize, + ): + mock_repo = MagicMock() + mock_repo.get.return_value = None + mock_repo.list_by_source_edit_plan.return_value = [] + mock_repo.list_latest_completed_preview.return_value = [] + mock_repo_cls.return_value = mock_repo + + mock_normalize.return_value = { + "cover": {"type": "ai_frame", "image_url": "https://oss.example.com/candidates/cover-1.jpg"} + } + + from app.api.routes.generation_cover import generate_cover + + result = generate_cover( + body=body, + template_id="template-z", + plan_id="plan-z", + services=(mock_template_svc, mock_plan_svc), + db=mock_db, + current_user=MagicMock(), + ) + + # 步骤D从 cover_candidates 第一个元素的 image_url 提取封面 + assert result.cover["image_url"] == "https://oss.example.com/candidates/cover-1.jpg" + # 验证 plan.config 被更新 + mock_plan_svc.update_plan_config.assert_called_once() + + def test_cover_url_found_via_cover_candidates_url_key(self): + """步骤D:cover_candidates 用 url 键(非 image_url)时,也能正确提取。""" + from unittest.mock import MagicMock, patch + + from app.api.routes.generation_cover import GenerateCoverRequest + + mock_plan = MagicMock() + mock_plan.config = { + "cover_candidates": [ + {"url": "https://oss.example.com/candidates/alt-cover.jpg"}, + ], + } + + mock_plan_svc = MagicMock() + mock_plan_svc.get_plan_or_raise.return_value = mock_plan + mock_template_svc = MagicMock() + mock_db = MagicMock() + + body = GenerateCoverRequest(cover_type="ai_frame") + + with ( + patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls, + patch("app.api.routes.generation_cover.normalize_plan_config") as mock_normalize, + ): + mock_repo = MagicMock() + mock_repo.get.return_value = None + mock_repo.list_by_source_edit_plan.return_value = [] + mock_repo.list_latest_completed_preview.return_value = [] + mock_repo_cls.return_value = mock_repo + + mock_normalize.return_value = { + "cover": {"type": "ai_frame", "image_url": "https://oss.example.com/candidates/alt-cover.jpg"} + } + + from app.api.routes.generation_cover import generate_cover + + result = generate_cover( + body=body, + template_id="template-w", + plan_id="plan-w", + services=(mock_template_svc, mock_plan_svc), + db=mock_db, + current_user=MagicMock(), + ) + + # 步骤D fallback 到 url 键 + assert result.cover["image_url"] == "https://oss.example.com/candidates/alt-cover.jpg" + + def test_cover_candidates_skips_non_dict_first_element(self): + """步骤D:cover_candidates 第一个元素不是 dict 时,安全跳过不崩溃。""" + from unittest.mock import MagicMock, patch + + from app.api.routes.generation_cover import GenerateCoverRequest + from fastapi import HTTPException + + mock_plan = MagicMock() + mock_plan.config = { + "cover_candidates": ["not-a-dict", 42, None], + } + + mock_plan_svc = MagicMock() + mock_plan_svc.get_plan_or_raise.return_value = mock_plan + mock_template_svc = MagicMock() + mock_db = MagicMock() + + body = GenerateCoverRequest(cover_type="ai_frame") + + with ( + patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls, + patch("packages.shared.storage.get_shared_storage_service") as mock_storage_getter, + ): + mock_repo = MagicMock() + mock_repo.get.return_value = None + mock_repo.list_by_source_edit_plan.return_value = [] + mock_repo.list_latest_completed_preview.return_value = [] + mock_repo_cls.return_value = mock_repo + + # storage fallback 也找不到封面 + mock_storage_svc = MagicMock() + mock_storage_svc.get_url.return_value = "" + mock_storage_getter.return_value = mock_storage_svc + + from app.api.routes.generation_cover import generate_cover + + # 所有步骤都失败,应返回 400 + with pytest.raises(HTTPException) as exc_info: + generate_cover( + body=body, + template_id="template-skip", + plan_id="plan-skip", + services=(mock_template_svc, mock_plan_svc), + db=mock_db, + current_user=MagicMock(), + ) + assert exc_info.value.status_code == 400 + class TestSourceEditPlanFallback: """测试步骤 2.5:通过 source_edit_plan_id 查找预览视频兜底逻辑。"""