0469272bd6
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 (push) Successful in 1s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
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 / PR Build API Image (pull_request) Successful in 22s
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 / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m59s
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 / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m16s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 3m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Validate - Security (push) Successful in 5m28s
AI Code Review / AI Code Review (pull_request) Failing after 5m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m6s
CI/CD Pipeline / Unit Tests (push) Successful in 8m54s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
140 lines
5.2 KiB
Python
Executable File
140 lines
5.2 KiB
Python
Executable File
"""#1743 批量生成无可用 plan 守卫。
|
||
|
||
新规则(P0 降重):count>1 批量生成时必须存在源 plan(前端传入或按模板兜底
|
||
解析到最新 plan),为每个变体独立选片;**无任何可用 plan 时直接 4xx 中断、
|
||
不创建任务**,严禁 N 个任务兜底共用同一 plan 产出同源成片。
|
||
N=1 单视频不受影响(无 plan 时走原有单任务流程)。
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
from fastapi import HTTPException
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
if str(REPO_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(REPO_ROOT))
|
||
|
||
|
||
def _make_user():
|
||
return SimpleNamespace(user=SimpleNamespace(id="user-1"))
|
||
|
||
|
||
def _make_request(count, **overrides):
|
||
from app.schemas.generation_task import CreateGenerationTaskRequest
|
||
|
||
fields = dict(
|
||
project_id="proj-1",
|
||
asset_library_id="lib-1",
|
||
strategy_id="one_take",
|
||
asset_ids=["a1"],
|
||
count=count,
|
||
source_edit_plan_id="",
|
||
)
|
||
fields.update(overrides)
|
||
return CreateGenerationTaskRequest(**fields)
|
||
|
||
|
||
def _common_patches(latest_plan=None):
|
||
"""构造通用 patch 上下文(repo/usecase/enqueue 等)。latest_plan 为模板兜底 plan 或 None。"""
|
||
repo = MagicMock()
|
||
repo.count_pending_by_user.return_value = 0
|
||
repo.count_pending_total.return_value = 0
|
||
repo.create.side_effect = lambda t: t
|
||
repo.update.side_effect = lambda t: t
|
||
|
||
created = []
|
||
|
||
def _fake_execute(cmd):
|
||
task = MagicMock()
|
||
task.id = f"task-{len(created) + 1}"
|
||
task.source_edit_plan_id = cmd.source_edit_plan_id
|
||
task.status = "pending"
|
||
task.progress = 0.0
|
||
task.strategy_id = "one_take"
|
||
task.error_message = ""
|
||
task.cover_url = ""
|
||
task.title_config = {}
|
||
task.created_at = None
|
||
task.batch_id = "batch-1"
|
||
created.append(task)
|
||
return task
|
||
|
||
db = MagicMock()
|
||
# 模板兜底查最新 plan:返回 latest_plan(None 表示查不到)
|
||
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = latest_plan
|
||
|
||
mock_uc = patch("app.api.routes.generation_tasks.CreateGenerationTaskUseCase")
|
||
other_patches = [
|
||
patch("app.api.routes.generation_tasks.safe_enqueue_generation_task", return_value=True),
|
||
patch("app.api.routes.generation_tasks._writeback_edit_plan_config"),
|
||
patch(
|
||
"app.api.routes.generation_tasks._resolve_project_and_library",
|
||
return_value=("proj-1", ""),
|
||
),
|
||
]
|
||
return repo, db, created, mock_uc, other_patches, _fake_execute
|
||
|
||
|
||
class TestBatchNoSourcePlanGuard:
|
||
def test_count3_without_any_plan_rejects_4xx_and_creates_nothing(self):
|
||
"""count=3 且无源 plan、模板兜底也查不到 → 400 中断,零任务创建(严禁同源成片)。"""
|
||
from app.api.routes.generation_tasks import create_generation_task
|
||
|
||
repo, db, created, mock_uc, other_patches, fake_exec = _common_patches(latest_plan=None)
|
||
MockUC = mock_uc.start()
|
||
MockUC.return_value.execute.side_effect = fake_exec
|
||
for p in other_patches:
|
||
p.start()
|
||
all_patches = [mock_uc] + other_patches
|
||
try:
|
||
with pytest.raises(HTTPException) as exc_info:
|
||
create_generation_task(
|
||
_make_request(3),
|
||
authenticated_user=_make_user(),
|
||
generation_task_repository=repo,
|
||
project_repository=MagicMock(),
|
||
asset_repository=MagicMock(),
|
||
asset_library_repository=MagicMock(),
|
||
db=db,
|
||
)
|
||
assert exc_info.value.status_code == 400
|
||
finally:
|
||
for p in reversed(all_patches):
|
||
p.stop()
|
||
assert len(created) == 0, "无 plan 批量必须零任务创建"
|
||
|
||
def test_count1_without_plan_still_works(self):
|
||
"""N=1 单视频无 plan 不触发批量守卫(向后兼容,不 4xx)。"""
|
||
from app.api.routes.generation_tasks import create_generation_task
|
||
|
||
repo, db, created, mock_uc, other_patches, fake_exec = _common_patches(latest_plan=None)
|
||
MockUC = mock_uc.start()
|
||
MockUC.return_value.execute.side_effect = fake_exec
|
||
for p in other_patches:
|
||
p.start()
|
||
all_patches = [mock_uc] + other_patches
|
||
try:
|
||
create_generation_task(
|
||
_make_request(1),
|
||
authenticated_user=_make_user(),
|
||
generation_task_repository=repo,
|
||
project_repository=MagicMock(),
|
||
asset_repository=MagicMock(),
|
||
asset_library_repository=MagicMock(),
|
||
db=db,
|
||
)
|
||
except HTTPException as e:
|
||
assert e.status_code != 400, f"N=1 不应被批量守卫拦截: {e.detail}"
|
||
except Exception:
|
||
# MagicMock 任务对象下游响应序列化可能抛 ValidationError 等,与批量守卫无关;
|
||
# 任务已在 usecase.execute 中创建,下方断言 created==1 即证明守卫未拦截。
|
||
pass
|
||
finally:
|
||
for p in reversed(all_patches):
|
||
p.stop()
|
||
assert len(created) == 1
|