feat: create端点兜底复用预览产物 + confirm同步标题
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 / 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 / Check if frontend-only change (pull_request) Successful in 39s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m27s
AI Code Review / AI Code Review (pull_request) Failing after 2m30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
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 / 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 / Check if frontend-only change (pull_request) Successful in 39s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m27s
AI Code Review / AI Code Review (pull_request) Failing after 2m30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
- create_generation_task: source_edit_plan_id非空且is_preview=False时, 查询该plan下已完成的预览任务,分辨率一致则直接mark_confirmed返回(秒出), 不创建新任务、不入队 - confirm_generation: ConfirmGenerationRequest增加custom_title字段, mark_confirmed时同步更新title_config,并回写EditPlan.config - GenerationTask.mark_confirmed: 增加title_config参数 - 8个回归测试覆盖兜底复用/分辨率不匹配/预览请求跳过/标题同步
This commit is contained in:
@@ -293,6 +293,89 @@ def create_generation_task(
|
||||
detail="当前项目没有符合条件的视频素材,请先上传并等待导入完成后再生成。",
|
||||
)
|
||||
|
||||
# ── 兜底复用预览产物 ──
|
||||
# 前端刷新后 previewTaskId 丢失,降级调 create 接口时,
|
||||
# 如果同一 edit_plan 有已完成的预览任务,直接复用(秒出)。
|
||||
if request.source_edit_plan_id and not request.is_preview:
|
||||
try:
|
||||
from packages.adapters.sqlalchemy_impl.models import (
|
||||
GenerationTaskModel,
|
||||
)
|
||||
|
||||
_preview_model = (
|
||||
db.query(GenerationTaskModel)
|
||||
.filter(
|
||||
GenerationTaskModel.source_edit_plan_id == request.source_edit_plan_id,
|
||||
GenerationTaskModel.is_preview.is_(True),
|
||||
GenerationTaskModel.status == "completed",
|
||||
GenerationTaskModel.created_by_user_id == authenticated_user.user.id,
|
||||
)
|
||||
.order_by(GenerationTaskModel.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
if _preview_model is not None:
|
||||
# 校验分辨率一致性(与 confirm 端点逻辑相同)
|
||||
req_w = request.output_width or 0
|
||||
req_h = request.output_height or 0
|
||||
src_w = getattr(_preview_model, "output_width", 0) or 0
|
||||
src_h = getattr(_preview_model, "output_height", 0) or 0
|
||||
resolution_match = (req_w == 0 or req_w == src_w) and (req_h == 0 or req_h == src_h)
|
||||
|
||||
if resolution_match:
|
||||
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
|
||||
_to_domain,
|
||||
)
|
||||
|
||||
preview_task = _to_domain(_preview_model)
|
||||
|
||||
# 如果传了标题,更新 title_config
|
||||
fallback_title_config = None
|
||||
if request.title_config and request.title_config.get("text", "").strip():
|
||||
fallback_title_config = dict(preview_task.title_config or {})
|
||||
fallback_title_config.update(request.title_config)
|
||||
|
||||
preview_task.mark_confirmed(
|
||||
cover_url=request.cover_url or preview_task.cover_url,
|
||||
output_width=request.output_width or preview_task.output_width,
|
||||
output_height=request.output_height or preview_task.output_height,
|
||||
title_config=fallback_title_config,
|
||||
)
|
||||
generation_task_repository.update(preview_task)
|
||||
|
||||
# 同步标题到 EditPlan.config
|
||||
if fallback_title_config:
|
||||
_writeback_edit_plan_config(
|
||||
plan_id=request.source_edit_plan_id,
|
||||
task_id=preview_task.id,
|
||||
title_config=fallback_title_config,
|
||||
db=db,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[生成任务] 兜底复用预览产物: preview_task_id=%s, plan_id=%s",
|
||||
preview_task.id,
|
||||
request.source_edit_plan_id,
|
||||
)
|
||||
return BatchGenerationTaskResponse(
|
||||
items=[_to_generation_task_response(preview_task)],
|
||||
total=1,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"[生成任务] 兜底复用跳过(分辨率不一致): plan_id=%s, src=%sx%s, req=%sx%s",
|
||||
request.source_edit_plan_id,
|
||||
src_w,
|
||||
src_h,
|
||||
req_w,
|
||||
req_h,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"[生成任务] 兜底复用预览产物异常(不影响主流程): plan_id=%s",
|
||||
request.source_edit_plan_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
use_case = CreateGenerationTaskUseCase(generation_task_repository)
|
||||
count = request.count
|
||||
created_tasks = []
|
||||
@@ -445,6 +528,7 @@ def confirm_generation(
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
generation_task_repository: Any = Depends(get_generation_task_repository),
|
||||
project_repository: Any = Depends(get_project_repository),
|
||||
db: Session = Depends(get_db_session),
|
||||
) -> BatchGenerationTaskResponse:
|
||||
"""确认生成 -- 复用预览渲染产物(预览与正式品质一致)。
|
||||
|
||||
@@ -473,12 +557,29 @@ def confirm_generation(
|
||||
resolution_match = (req_w == 0 or req_w == src_w) and (req_h == 0 or req_h == src_h)
|
||||
|
||||
if resolution_match:
|
||||
# 如果用户传了 custom_title,同步更新 title_config
|
||||
confirmed_title_config = None
|
||||
if request.custom_title and request.custom_title.strip():
|
||||
confirmed_title_config = dict(getattr(source_task, "title_config", {}) or {})
|
||||
confirmed_title_config["text"] = request.custom_title.strip()
|
||||
|
||||
source_task.mark_confirmed(
|
||||
cover_url=request.cover_url,
|
||||
output_width=request.output_width,
|
||||
output_height=request.output_height,
|
||||
title_config=confirmed_title_config,
|
||||
)
|
||||
generation_task_repository.update(source_task)
|
||||
|
||||
# 同步标题到 EditPlan.config
|
||||
if confirmed_title_config and source_task.source_edit_plan_id:
|
||||
_writeback_edit_plan_config(
|
||||
plan_id=source_task.source_edit_plan_id,
|
||||
task_id=source_task.id,
|
||||
title_config=confirmed_title_config,
|
||||
db=db,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[确认生成] 复用预览产物: task_id=%s, user_id=%s",
|
||||
task_id,
|
||||
|
||||
@@ -10,6 +10,7 @@ class ConfirmGenerationRequest(BaseModel):
|
||||
output_width: int = Field(default=1080, ge=100, description="输出视频宽度")
|
||||
output_height: int = Field(default=1920, ge=100, description="输出视频高度")
|
||||
cover_url: str = Field(default="", description="自定义封面图片 URL")
|
||||
custom_title: str = Field(default="", description="用户自定义标题文本,非空时同步到任务和编辑计划")
|
||||
|
||||
|
||||
class CreateGenerationTaskRequest(BaseModel):
|
||||
|
||||
@@ -306,6 +306,7 @@ class GenerationTask:
|
||||
extra_meta: dict | None = None,
|
||||
output_width: int = 0,
|
||||
output_height: int = 0,
|
||||
title_config: dict | None = None,
|
||||
) -> None:
|
||||
"""将预览任务确认为正式产出。
|
||||
|
||||
@@ -319,6 +320,8 @@ class GenerationTask:
|
||||
self.output_width = output_width
|
||||
if output_height > 0:
|
||||
self.output_height = output_height
|
||||
if title_config:
|
||||
self.title_config = dict(title_config)
|
||||
if extra_meta:
|
||||
self.extra_meta.update(extra_meta)
|
||||
self.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
"""create 端点兜底复用预览产物 + confirm 标题同步 — 单元测试."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
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")
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "api"))
|
||||
|
||||
from packages.domain.generation_task import GenerationTask, GenerationTaskStatus
|
||||
|
||||
|
||||
# ── Stubs ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class StubGenerationTaskRepository:
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[str, Any] = {}
|
||||
|
||||
def create(self, task: Any) -> Any:
|
||||
self._store[task.id] = task
|
||||
return task
|
||||
|
||||
def get(self, task_id: str) -> Optional[Any]:
|
||||
return self._store.get(task_id)
|
||||
|
||||
def update(self, task: Any) -> Any:
|
||||
self._store[task.id] = task
|
||||
return task
|
||||
|
||||
def list_by_user(self, user_id: str) -> list[Any]:
|
||||
return [t for t in self._store.values() if t.created_by_user_id == user_id]
|
||||
|
||||
def count_pending_by_user(self, user_id: str) -> int:
|
||||
return len([
|
||||
t for t in self._store.values()
|
||||
if t.created_by_user_id == user_id and t.status == GenerationTaskStatus.PENDING
|
||||
])
|
||||
|
||||
def count_pending_total(self) -> int:
|
||||
return len([t for t in self._store.values() if t.status == GenerationTaskStatus.PENDING])
|
||||
|
||||
def count_by_user(self, user_id: str) -> int:
|
||||
return len([t for t in self._store.values() if t.created_by_user_id == user_id])
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeProject:
|
||||
id: str = "project-001"
|
||||
owner_user_id: str = "user-001"
|
||||
shared_users: list[str] = field(default_factory=list)
|
||||
name: str = "Test Project"
|
||||
|
||||
def can_access(self, user_id: str) -> bool:
|
||||
return user_id == self.owner_user_id or user_id in self.shared_users
|
||||
|
||||
|
||||
class StubProjectRepository:
|
||||
def __init__(self) -> None:
|
||||
self._projects: dict[str, FakeProject] = {}
|
||||
|
||||
def add(self, project: FakeProject) -> None:
|
||||
self._projects[project.id] = project
|
||||
|
||||
def find_by_id(self, project_id: str) -> Optional[FakeProject]:
|
||||
return self._projects.get(project_id)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeUser:
|
||||
id: str = "user-001"
|
||||
email: str = "test@example.com"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeAuthenticatedUser:
|
||||
user: FakeUser = field(default_factory=FakeUser)
|
||||
session_id: str | None = None
|
||||
token_type: str | None = None
|
||||
|
||||
|
||||
def _make_preview_task(**kwargs: Any) -> GenerationTask:
|
||||
defaults = dict(
|
||||
id="preview-task-001",
|
||||
project_id="project-001",
|
||||
asset_library_id="library-001",
|
||||
strategy_id="one_take",
|
||||
voice_library_id="",
|
||||
template_id="tmpl-001",
|
||||
asset_ids=["asset-1"],
|
||||
title_ids=[],
|
||||
voice_ids=[],
|
||||
status=GenerationTaskStatus.COMPLETED,
|
||||
progress=100.0,
|
||||
result_count=1,
|
||||
error_message="",
|
||||
created_by_user_id="user-001",
|
||||
source_edit_plan_id="plan-001",
|
||||
asset_select_mode="all",
|
||||
is_preview=True,
|
||||
source_task_id="",
|
||||
output_width=1080,
|
||||
output_height=1920,
|
||||
cover_url="",
|
||||
video_title="",
|
||||
resolution="",
|
||||
bgm_config={},
|
||||
title_config={"text": "预览标题"},
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return GenerationTask(**defaults)
|
||||
|
||||
|
||||
def _make_db_with_preview(preview: GenerationTask):
|
||||
"""Create a mock DB that returns a model-like object for the preview."""
|
||||
db = MagicMock()
|
||||
mock_model = MagicMock()
|
||||
mock_model.id = preview.id
|
||||
mock_model.output_width = preview.output_width
|
||||
mock_model.output_height = preview.output_height
|
||||
# Set up chain: db.query(...).filter(...).order_by(...).first()
|
||||
chain = db.query.return_value
|
||||
chain.filter.return_value = chain
|
||||
chain.order_by.return_value = chain
|
||||
chain.first.return_value = mock_model
|
||||
return db, mock_model
|
||||
|
||||
|
||||
def _make_db_empty():
|
||||
"""Create a mock DB that returns None (no preview found)."""
|
||||
db = MagicMock()
|
||||
chain = db.query.return_value
|
||||
chain.filter.return_value = chain
|
||||
chain.order_by.return_value = chain
|
||||
chain.first.return_value = None
|
||||
return db
|
||||
|
||||
|
||||
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gen_task_repo() -> StubGenerationTaskRepository:
|
||||
return StubGenerationTaskRepository()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_repo() -> StubProjectRepository:
|
||||
repo = StubProjectRepository()
|
||||
repo.add(FakeProject())
|
||||
return repo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
project_repo: StubProjectRepository,
|
||||
) -> FastAPI:
|
||||
from app.api.routes.generation_tasks import router
|
||||
from app.auth import get_current_user
|
||||
from app.dependencies import (
|
||||
get_asset_library_repository,
|
||||
get_asset_repository,
|
||||
get_db_session,
|
||||
get_generated_video_repository,
|
||||
get_generation_task_repository,
|
||||
get_project_repository,
|
||||
)
|
||||
|
||||
test_app = FastAPI()
|
||||
test_app.include_router(router, prefix="/api/v1/generation")
|
||||
|
||||
test_app.dependency_overrides[get_current_user] = lambda: FakeAuthenticatedUser()
|
||||
test_app.dependency_overrides[get_generation_task_repository] = lambda: gen_task_repo
|
||||
test_app.dependency_overrides[get_project_repository] = lambda: project_repo
|
||||
test_app.dependency_overrides[get_asset_library_repository] = lambda: MagicMock()
|
||||
test_app.dependency_overrides[get_asset_repository] = lambda: MagicMock()
|
||||
test_app.dependency_overrides[get_generated_video_repository] = lambda: MagicMock()
|
||||
# db_session will be overridden per-test
|
||||
|
||||
yield test_app
|
||||
test_app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def _make_client(app: FastAPI, db: MagicMock) -> TestClient:
|
||||
from app.dependencies import get_db_session
|
||||
app.dependency_overrides[get_db_session] = lambda: db
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
# ── Domain: mark_confirmed with title_config ─────────────────────────────────
|
||||
|
||||
|
||||
class TestMarkConfirmedTitleConfig:
|
||||
def test_mark_confirmed_sets_title_config(self):
|
||||
task = _make_preview_task()
|
||||
task.mark_confirmed(title_config={"text": "新标题"})
|
||||
assert task.is_preview is False
|
||||
assert task.title_config["text"] == "新标题"
|
||||
|
||||
def test_mark_confirmed_without_title_config_preserves_existing(self):
|
||||
task = _make_preview_task(title_config={"text": "原标题"})
|
||||
task.mark_confirmed()
|
||||
assert task.title_config["text"] == "原标题"
|
||||
|
||||
|
||||
# ── Create endpoint fallback ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCreateEndpointFallback:
|
||||
"""create 端点兜底复用预览产物。"""
|
||||
|
||||
def test_reuse_completed_preview(
|
||||
self,
|
||||
app: FastAPI,
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
):
|
||||
"""带 source_edit_plan_id + is_preview=False → 复用已完成预览"""
|
||||
preview = _make_preview_task()
|
||||
gen_task_repo.create(preview)
|
||||
db, _ = _make_db_with_preview(preview)
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.generation_task_repository._to_domain",
|
||||
return_value=preview,
|
||||
), patch(
|
||||
"app.api.routes.generation_tasks._writeback_edit_plan_config",
|
||||
):
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
"/api/v1/generation/tasks",
|
||||
json={
|
||||
"project_id": "project-001",
|
||||
"template_id": "tmpl-001",
|
||||
"asset_ids": ["asset-1"],
|
||||
"source_edit_plan_id": "plan-001",
|
||||
"is_preview": False,
|
||||
"output_width": 1080,
|
||||
"output_height": 1920,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["id"] == preview.id
|
||||
assert data["items"][0]["is_preview"] is False
|
||||
|
||||
def test_no_preview_found_creates_new_task(
|
||||
self,
|
||||
app: FastAPI,
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
):
|
||||
"""没有已完成预览 → 正常创建新任务"""
|
||||
db = _make_db_empty()
|
||||
|
||||
with patch("app.api.routes.generation_tasks.safe_enqueue_generation_task", return_value=True):
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
"/api/v1/generation/tasks",
|
||||
json={
|
||||
"project_id": "project-001",
|
||||
"template_id": "tmpl-001",
|
||||
"asset_ids": ["asset-1"],
|
||||
"source_edit_plan_id": "plan-002",
|
||||
"is_preview": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["total"] == 1
|
||||
assert data["items"][0]["id"] != "preview-task-001"
|
||||
assert data["items"][0]["is_preview"] is False
|
||||
|
||||
def test_preview_request_does_not_use_fallback(
|
||||
self,
|
||||
app: FastAPI,
|
||||
):
|
||||
"""is_preview=True → 不走兜底,正常创建预览任务"""
|
||||
db = _make_db_empty()
|
||||
|
||||
with patch("app.api.routes.generation_tasks.safe_enqueue_generation_task", return_value=True):
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
"/api/v1/generation/tasks",
|
||||
json={
|
||||
"project_id": "project-001",
|
||||
"template_id": "tmpl-001",
|
||||
"asset_ids": ["asset-1"],
|
||||
"source_edit_plan_id": "plan-001",
|
||||
"is_preview": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["items"][0]["is_preview"] is True
|
||||
# 兜底查询 GenerationTaskModel 不应被调用(只可能查 EditPlanModel 做自动关联)
|
||||
from packages.adapters.sqlalchemy_impl.models import GenerationTaskModel
|
||||
for call_args in db.query.call_args_list:
|
||||
assert call_args[0][0] is not GenerationTaskModel, "fallback should not query GenerationTaskModel for preview requests"
|
||||
|
||||
def test_fallback_resolution_mismatch_creates_new(
|
||||
self,
|
||||
app: FastAPI,
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
):
|
||||
"""兜底找到预览但分辨率不一致 → 跳过复用,创建新任务"""
|
||||
preview = _make_preview_task(output_width=1080, output_height=1920)
|
||||
gen_task_repo.create(preview)
|
||||
db, _ = _make_db_with_preview(preview)
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.generation_task_repository._to_domain",
|
||||
return_value=preview,
|
||||
), patch("app.api.routes.generation_tasks.safe_enqueue_generation_task", return_value=True):
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
"/api/v1/generation/tasks",
|
||||
json={
|
||||
"project_id": "project-001",
|
||||
"template_id": "tmpl-001",
|
||||
"asset_ids": ["asset-1"],
|
||||
"source_edit_plan_id": "plan-001",
|
||||
"is_preview": False,
|
||||
"output_width": 1920,
|
||||
"output_height": 1080,
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["items"][0]["id"] != preview.id
|
||||
|
||||
|
||||
# ── Confirm endpoint title sync ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestConfirmTitleSync:
|
||||
"""confirm 端点 custom_title 同步到 title_config。"""
|
||||
|
||||
def test_confirm_with_custom_title_updates_title_config(
|
||||
self,
|
||||
app: FastAPI,
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
):
|
||||
"""传了 custom_title → title_config.text 被更新"""
|
||||
preview = _make_preview_task(
|
||||
title_config={"text": "旧标题", "font_size": 32},
|
||||
source_edit_plan_id="plan-001",
|
||||
)
|
||||
gen_task_repo.create(preview)
|
||||
db = _make_db_empty()
|
||||
|
||||
with patch("app.api.routes.generation_tasks._writeback_edit_plan_config"):
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
f"/api/v1/generation/tasks/{preview.id}/confirm",
|
||||
json={
|
||||
"output_width": 1080,
|
||||
"output_height": 1920,
|
||||
"custom_title": "新标题",
|
||||
},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
item = resp.json()["items"][0]
|
||||
assert item["title_config"]["text"] == "新标题"
|
||||
assert item["title_config"]["font_size"] == 32
|
||||
|
||||
def test_confirm_without_custom_title_preserves_title(
|
||||
self,
|
||||
app: FastAPI,
|
||||
gen_task_repo: StubGenerationTaskRepository,
|
||||
):
|
||||
"""没传 custom_title → title_config 不变"""
|
||||
preview = _make_preview_task(title_config={"text": "原标题"})
|
||||
gen_task_repo.create(preview)
|
||||
db = _make_db_empty()
|
||||
|
||||
client = _make_client(app, db)
|
||||
resp = client.post(
|
||||
f"/api/v1/generation/tasks/{preview.id}/confirm",
|
||||
json={"output_width": 1080, "output_height": 1920},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["items"][0]["title_config"]["text"] == "原标题"
|
||||
Reference in New Issue
Block a user