Files
xiaoxia-saas/tests/unit/test_cover_awaiting_fallback.py
T
xiaoxia cb4f076119
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m44s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m2s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m5s
AI Code Review / AI Code Review (pull_request) Successful in 5m16s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m39s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 8m35s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m51s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 13m29s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m54s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 33s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m7s
fix(generation): #2028 awaiting_cover 预览可见 + clips/from-assets 零时长容错(rebase on #2030)
- 后端 _get_task_video_url:DB 查不到 GeneratedVideo 时回退读 task.extra_meta.rendered_output.file_url,封面生成不再 404
- 后端 results:awaiting_cover 且无 DB 记录时,从 extra_meta 合成 preview-{task_id} 响应
- 后端 finalize 服务:支持 custom_title 自定义标题,幂等分支也更新名称
- 后端 clips/from-assets:asset=None 或 duration≤0 跳过;score_asset/scene_points 异常兜底不阻断;全素材无效返友好 400「素材尚未完成分析,请稍后重试」
- 前端轮询:pollSingleTask 把 awaiting_cover 视为可预览终态;BatchTaskState.status 加 awaiting_cover;7 处 completed 过滤加 OR awaiting_cover
- 前端 finalize:单步调 finalizeGeneration(custom_title 透传),不再先 confirm 再 finalize;响应字段补全 project_id/name/file_size/duration/thumbnail_url/width/height/fps
- 单测 11 个新增/更新,本地全过
2026-09-24 17:48:05 +08:00

168 lines
6.3 KiB
Python

"""#2028: generation_cover._get_task_video_url 兜底逻辑测试。"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import pytest
class TestGetTaskVideoUrlAwaitingCoverFallback:
def test_returns_url_from_rendered_output_when_awaiting_cover(self):
from app.api.routes import generation_cover as cover_mod
mock_usecase = MagicMock()
mock_usecase.execute.return_value = []
mock_task = MagicMock()
mock_task.status.value = "awaiting_cover"
mock_task.extra_meta = {"rendered_output": {"file_url": "oss://generated/awaiting.mp4"}}
mock_task_repo_instance = MagicMock()
mock_task_repo_instance.get.return_value = mock_task
mock_db = MagicMock()
with (
patch.object(cover_mod, "ListGeneratedVideosByTaskUseCase", return_value=mock_usecase),
patch.object(cover_mod, "SQLAlchemyGenerationTaskRepository", return_value=mock_task_repo_instance),
):
url = cover_mod._get_task_video_url("task-aw1", mock_db)
assert url == "oss://generated/awaiting.mp4"
def test_returns_none_when_status_not_awaiting_cover(self):
from app.api.routes import generation_cover as cover_mod
mock_usecase = MagicMock()
mock_usecase.execute.return_value = []
mock_task = MagicMock()
mock_task.status.value = "running"
mock_task.extra_meta = {"rendered_output": {"file_url": "oss://x.mp4"}}
mock_task_repo_instance = MagicMock()
mock_task_repo_instance.get.return_value = mock_task
mock_db = MagicMock()
with (
patch.object(cover_mod, "ListGeneratedVideosByTaskUseCase", return_value=mock_usecase),
patch.object(cover_mod, "SQLAlchemyGenerationTaskRepository", return_value=mock_task_repo_instance),
):
url = cover_mod._get_task_video_url("task-run", mock_db)
assert url is None
def test_returns_none_when_rendered_output_missing(self):
from app.api.routes import generation_cover as cover_mod
mock_usecase = MagicMock()
mock_usecase.execute.return_value = []
mock_task = MagicMock()
mock_task.status.value = "awaiting_cover"
mock_task.extra_meta = {}
mock_task_repo_instance = MagicMock()
mock_task_repo_instance.get.return_value = mock_task
mock_db = MagicMock()
with (
patch.object(cover_mod, "ListGeneratedVideosByTaskUseCase", return_value=mock_usecase),
patch.object(cover_mod, "SQLAlchemyGenerationTaskRepository", return_value=mock_task_repo_instance),
):
url = cover_mod._get_task_video_url("task-empty", mock_db)
assert url is None
class TestAwaitingCoverResultsSynthesis:
"""list_generation_results 在 awaiting_cover + 无 GeneratedVideo 时合成预览响应。"""
def _invoke(self, task, storage_service):
from app.api.routes import generation_tasks as gt_mod
mock_auth = MagicMock()
mock_auth.user.id = "u1"
mock_task_repo = MagicMock()
mock_task_repo.get.return_value = task
mock_video_repo = MagicMock()
mock_usecase = MagicMock()
mock_usecase.execute.return_value = []
mock_project_repo = MagicMock()
with (
patch.object(gt_mod, "check_project_access", return_value=None),
patch.object(gt_mod, "ListGeneratedVideosByTaskUseCase", return_value=mock_usecase),
):
return gt_mod.list_generation_results(
task_id=task.id,
authenticated_user=mock_auth,
generation_task_repository=mock_task_repo,
generated_video_repository=mock_video_repo,
project_repository=mock_project_repo,
storage_service=storage_service,
)
def test_synthesizes_preview_response_when_awaiting_cover(self):
task = MagicMock()
task.id = "task-syn-1"
task.project_id = "proj1"
task.status.value = "awaiting_cover"
task.cover_url = ""
task.extra_meta = {
"rendered_output": {
"file_url": "oss://bucket/v.mp4",
"file_size": 2048,
"duration": 10.5,
"width": 1080,
"height": 1920,
"fps": 30.0,
"name": "合成预览",
"mode": "random",
"thumbnail_url": "https://cdn/t.jpg",
}
}
task.updated_at = None
task.created_at = None
storage = MagicMock()
storage.get_download_url.return_value = "https://signed/v.mp4"
resp = self._invoke(task, storage)
assert len(resp.items) == 1
item = resp.items[0]
assert item.id == "preview-task-syn-1"
assert item.name == "合成预览"
assert item.file_size == 2048
assert item.duration == 10.5
assert item.width == 1080
assert item.height == 1920
assert item.fps == 30.0
assert item.download_url == "https://signed/v.mp4"
def test_http_file_url_used_directly(self):
task = MagicMock()
task.id = "task-httpx"
task.project_id = "p"
task.status.value = "awaiting_cover"
task.cover_url = ""
task.extra_meta = {"rendered_output": {"file_url": "https://cdn.example.com/v.mp4", "name": ""}}
task.updated_at = None
task.created_at = None
storage = MagicMock()
resp = self._invoke(task, storage)
assert resp.items[0].download_url == "https://cdn.example.com/v.mp4"
storage.get_download_url.assert_not_called()
assert resp.items[0].name.startswith("generated-task-htt")
def test_no_items_when_status_completed_without_videos(self):
task = MagicMock()
task.id = "task-done"
task.project_id = "p"
task.status.value = "completed"
task.extra_meta = {"rendered_output": {"file_url": "oss://x.mp4"}}
storage = MagicMock()
resp = self._invoke(task, storage)
assert resp.items == []
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))