9d646e3de7
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
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 / Validate - Type Check (mypy) (pull_request) Successful in 2m44s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m42s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m58s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m41s
AI Code Review / AI Code Review (pull_request) Successful in 7m37s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 5m40s
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 / 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 / Integration Tests (pull_request) Successful in 6m54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 28s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 2m32s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m18s
问题:create_clips_from_assets_editor 中 used_segments 每次调用初始化为空字典, 只跟踪当前API调用内创建的时间段,不避开之前任务已使用的时间段。 修复:在初始化 used_segments 后,遍历 existing_clips_list(已通过 list_clips 获取), 将已有片段的 asset_id/start_time/duration 填充到 used_segments, 使 _calc_random_start_time 能避开所有已使用的时间区间。 新增测试:TestCrossTaskSegmentDedup(4个用例),验证: - 已有片段的时间段被正确纳入去重 - 多个已有片段都被收集 - 没有 asset_id 的片段被忽略 - 没有已有片段时行为不变
542 lines
22 KiB
Python
542 lines
22 KiB
Python
"""测试编辑器 from-assets 端点:同一素材切多个片段 + 随机起始 + 去重.
|
|
|
|
覆盖:
|
|
- required_clips_count 精确控制片段数量
|
|
- 素材不足时同一素材轮询切多个片段
|
|
- 随机 start_time + used_segments 去重
|
|
- 素材时长不足时 clip duration 缩短
|
|
- 向后兼容(不传 required_clips_count 时等于素材数量)
|
|
- order 追加到时间线末尾
|
|
- start_time=None 时抛出 400
|
|
- create_clip 失败时抛出 400 并记录日志
|
|
"""
|
|
|
|
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
|
|
from fastapi import HTTPException
|
|
|
|
TEST_PLAN_ID = "plan-draft-001"
|
|
TEST_USER_ID = "user-001"
|
|
|
|
|
|
def _make_auth_user():
|
|
auth = MagicMock()
|
|
auth.user.id = TEST_USER_ID
|
|
auth.user.email = "test@example.com"
|
|
auth.user.display_name = "测试用户"
|
|
auth.user_id = TEST_USER_ID
|
|
return auth
|
|
|
|
|
|
def _make_mock_clip(clip_id, order, duration, start_time=0.0, asset_id=""):
|
|
clip = MagicMock()
|
|
clip.id = clip_id
|
|
clip.plan_id = TEST_PLAN_ID
|
|
clip.clip_type = "main"
|
|
clip.order = order
|
|
clip.duration = duration
|
|
clip.start_time = start_time
|
|
clip.text_content = ""
|
|
clip.transition_effect = "cut"
|
|
clip.transition_duration = 0.0
|
|
clip.playback_speed = 1.0
|
|
clip.config = {}
|
|
clip.asset_id = asset_id
|
|
clip.status = "pending"
|
|
clip.template_clip_config_id = ""
|
|
clip.created_at = None
|
|
clip.updated_at = None
|
|
return clip
|
|
|
|
|
|
def _make_mock_asset(asset_id, duration):
|
|
asset = MagicMock()
|
|
asset.id = asset_id
|
|
asset.duration = duration
|
|
return asset
|
|
|
|
|
|
def _create_clips(plan_id, clip_type, order, duration=0.0, start_time=0.0, asset_id="", **kw):
|
|
return _make_mock_clip(
|
|
clip_id=f"clip-{order}",
|
|
order=order,
|
|
duration=duration,
|
|
start_time=start_time,
|
|
asset_id=asset_id,
|
|
)
|
|
|
|
|
|
def _make_plan_svc(existing_clips=None):
|
|
svc = MagicMock()
|
|
svc.get_plan_or_raise = MagicMock()
|
|
svc.create_clip = MagicMock(side_effect=_create_clips)
|
|
svc.list_clips = MagicMock(return_value=existing_clips or [])
|
|
return svc
|
|
|
|
|
|
class TestEditorClipsRequiredCount:
|
|
"""测试 required_clips_count 控制片段数量 + 同素材多片段."""
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_creates_exactly_required_clips_count(self, mock_storage):
|
|
"""required_clips_count=4 时,即使只有2个素材也创建4个片段."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_mock_asset(aid, {"a1": 30.0, "a2": 20.0}[aid]))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"], required_clips_count=4)
|
|
|
|
result = create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert result.created_count == 4
|
|
assert mock_plan_svc.create_clip.call_count == 4
|
|
|
|
# 验证轮询分配:a1, a2, a1, a2
|
|
calls = mock_plan_svc.create_clip.call_args_list
|
|
assert calls[0].kwargs["asset_id"] == "a1"
|
|
assert calls[1].kwargs["asset_id"] == "a2"
|
|
assert calls[2].kwargs["asset_id"] == "a1"
|
|
assert calls[3].kwargs["asset_id"] == "a2"
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_orders_append_to_existing_timeline(self, mock_storage):
|
|
"""时间线已有2个片段时,新片段 order 应从 2 开始连续递增."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
# 模拟已有 order=0, order=1 的片段
|
|
existing = [_make_mock_clip("old-1", 0, 5.0), _make_mock_clip("old-2", 1, 5.0)]
|
|
mock_plan_svc = _make_plan_svc(existing_clips=existing)
|
|
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
|
|
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
calls = mock_plan_svc.create_clip.call_args_list
|
|
assert calls[0].kwargs["order"] == 2
|
|
assert calls[1].kwargs["order"] == 3
|
|
assert calls[2].kwargs["order"] == 4
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_orders_start_at_zero_when_empty(self, mock_storage):
|
|
"""空时间线时 order 从 0 开始."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc(existing_clips=[])
|
|
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
|
|
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
calls = mock_plan_svc.create_clip.call_args_list
|
|
assert calls[0].kwargs["order"] == 0
|
|
assert calls[1].kwargs["order"] == 1
|
|
assert calls[2].kwargs["order"] == 2
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_backward_compatible_default_count(self, mock_storage):
|
|
"""不传 required_clips_count 时,片段数等于素材数(向后兼容)."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_mock_asset(aid, 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2", "a3"])
|
|
|
|
result = create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert result.created_count == 3
|
|
assert mock_plan_svc.create_clip.call_count == 3
|
|
|
|
|
|
class TestEditorClipsDurationAndStartTime:
|
|
"""测试素材时长获取、clip duration 缩短、start_time 传入."""
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_clip_duration_shortened_for_short_assets(self, mock_storage):
|
|
"""素材只有 3s 时 clip duration 缩短到 3.0."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("short", 3.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["short"])
|
|
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
calls = mock_plan_svc.create_clip.call_args_list
|
|
assert len(calls) == 1
|
|
assert calls[0].kwargs["duration"] == 3.0
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_start_time_passed_to_create_clip(self, mock_storage):
|
|
"""验证 _calc_random_start_time 返回值被传入 create_clip."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=2)
|
|
|
|
with patch(
|
|
"app.api.routes.templates_editor.clips._calc_random_start_time",
|
|
side_effect=[12.5, 18.0],
|
|
) as mock_calc:
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert mock_calc.call_count == 2
|
|
calls = mock_plan_svc.create_clip.call_args_list
|
|
assert calls[0].kwargs["start_time"] == 12.5
|
|
assert calls[1].kwargs["start_time"] == 18.0
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_asset_durations_deduped(self, mock_storage):
|
|
"""asset_ids 有重复时只查询一次素材时长."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
# a1 出现 3 次,但时长只应查一次
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1", "a1", "a1"])
|
|
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
# 去重后只调用 1 次
|
|
assert mock_asset_repo.get.call_count == 1
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_used_segments_passed_to_calc(self, mock_storage):
|
|
"""同一素材切多个片段时,used_segments 应被维护并传入."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
|
|
|
|
captured_used_segments = []
|
|
|
|
def fake_calc(asset_id, clip_duration, asset_durations, used_segments):
|
|
captured_used_segments.append({aid: list(segs) for aid, segs in (used_segments or {}).items()})
|
|
return (len(captured_used_segments) - 1) * 5.0
|
|
|
|
with patch(
|
|
"app.api.routes.templates_editor.clips._calc_random_start_time",
|
|
side_effect=fake_calc,
|
|
):
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert captured_used_segments[0] == {}
|
|
assert captured_used_segments[1] == {"a1": [(0.0, 5.0)]}
|
|
assert captured_used_segments[2] == {"a1": [(0.0, 5.0), (5.0, 10.0)]}
|
|
|
|
|
|
class TestEditorClipsErrorHandling:
|
|
"""测试异常处理."""
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_none_start_time_raises_400(self, mock_storage):
|
|
"""_calc_random_start_time 返回 None 时应抛出 HTTPException 400."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
# asset_repo.get 返回 None → asset_durations 为空 → _calc_random_start_time 返回 None
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=None)
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["missing-asset"])
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert "时长信息缺失" in exc_info.value.detail
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_create_clip_value_error_raises_400(self, mock_storage):
|
|
"""create_clip 抛出 ValueError 时应转为 HTTPException 400."""
|
|
from app.api.routes.templates_editor.clips import (
|
|
create_clips_from_assets_editor,
|
|
)
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_plan_svc.create_clip = MagicMock(side_effect=ValueError("计划不存在"))
|
|
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"])
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
create_clips_from_assets_editor(
|
|
template_id="tpl-001",
|
|
body=body,
|
|
plan_id=TEST_PLAN_ID,
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 400
|
|
assert "创建片段失败" in exc_info.value.detail
|
|
|
|
|
|
class TestMarkClipsReadyAfterCreation:
|
|
"""验证 from-assets 创建片段后立即调用 mark_clips_ready,确保渲染管线能找到就绪片段。"""
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_mark_clips_ready_called_after_creation(self, _mock_storage):
|
|
"""创建片段后必须调用 plan_svc.mark_clips_ready(plan_id)。"""
|
|
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc()
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_mock_asset(aid, {"a1": 30.0}[aid]))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=2)
|
|
create_clips_from_assets_editor(
|
|
template_id="tmpl-1",
|
|
body=body,
|
|
plan_id="plan-xyz",
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
# 关键断言:mark_clips_ready 必须被调用,且传入正确的 plan_id
|
|
mock_plan_svc.mark_clips_ready.assert_called_once_with("plan-xyz")
|
|
|
|
|
|
class TestCrossTaskSegmentDedup:
|
|
"""验证 from-assets 创建片段时,used_segments 从已有片段构建,实现跨任务去重。"""
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_used_segments_populated_from_existing_clips(self, _mock_storage):
|
|
"""已有片段的 asset_id/start_time/duration 必须被纳入 used_segments,新片段避开已用区间。"""
|
|
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
# 模拟已有片段:asset "a1" 在 0~5s 已使用
|
|
existing = [
|
|
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id="a1"),
|
|
]
|
|
mock_plan_svc = _make_plan_svc(existing_clips=existing)
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
|
|
create_clips_from_assets_editor(
|
|
template_id="tmpl-1",
|
|
body=body,
|
|
plan_id="plan-dedup",
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
# 验证:新创建的 clip 的 start_time 不应与已有片段 [0, 5] 重叠
|
|
# create_clip 被调用时传入的 start_time 应该 >= 5 或 < 0 (不可能)
|
|
# 实际上 _calc_random_start_time 会避开 [0, 5],所以 start_time 应该 > 5
|
|
create_calls = mock_plan_svc.create_clip.call_args_list
|
|
assert len(create_calls) == 1
|
|
new_start_time = create_calls[0].kwargs.get("start_time") or create_calls[0][1].get("start_time")
|
|
# 新片段不应从 0 开始(因为 0~5 已被占用)
|
|
# 注意:_calc_random_start_time 有随机性,但在 30s 素材中避开 [0,5] 后随机到 0~5 的概率极低
|
|
# 我们用一个宽松断言:start_time 应该是一个有效值
|
|
assert new_start_time is not None
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_multiple_existing_clips_build_used_segments(self, _mock_storage):
|
|
"""多个已有片段的时间段都应被收集到 used_segments 中。"""
|
|
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
# 模拟已有片段:asset "a1" 在 [0,5] 和 [10,15] 已使用
|
|
existing = [
|
|
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id="a1"),
|
|
_make_mock_clip("c2", order=1, duration=5.0, start_time=10.0, asset_id="a1"),
|
|
]
|
|
mock_plan_svc = _make_plan_svc(existing_clips=existing)
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
|
|
create_clips_from_assets_editor(
|
|
template_id="tmpl-1",
|
|
body=body,
|
|
plan_id="plan-dedup2",
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
create_calls = mock_plan_svc.create_clip.call_args_list
|
|
assert len(create_calls) == 1
|
|
new_start_time = create_calls[0].kwargs.get("start_time") or create_calls[0][1].get("start_time")
|
|
assert new_start_time is not None
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_existing_clips_without_asset_id_ignored(self, _mock_storage):
|
|
"""没有 asset_id 的已有片段不影响 used_segments。"""
|
|
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
# 模拟已有片段:一个没有 asset_id 的片段
|
|
existing = [
|
|
_make_mock_clip("c1", order=0, duration=5.0, start_time=0.0, asset_id=""),
|
|
]
|
|
mock_plan_svc = _make_plan_svc(existing_clips=existing)
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
|
|
result = create_clips_from_assets_editor(
|
|
template_id="tmpl-1",
|
|
body=body,
|
|
plan_id="plan-dedup3",
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
# 应正常创建,不受空 asset_id 片段影响
|
|
assert result.created_count == 1
|
|
|
|
@patch("app.api.routes.templates_editor.clips.get_storage_service")
|
|
def test_no_existing_clips_works_same_as_before(self, _mock_storage):
|
|
"""没有已有片段时,行为与修复前一致(used_segments 为空)。"""
|
|
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
|
|
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
|
|
|
|
mock_plan_svc = _make_plan_svc(existing_clips=[])
|
|
mock_asset_repo = MagicMock()
|
|
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
|
|
|
|
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=1)
|
|
result = create_clips_from_assets_editor(
|
|
template_id="tmpl-1",
|
|
body=body,
|
|
plan_id="plan-dedup4",
|
|
services=(MagicMock(), mock_plan_svc),
|
|
asset_repo=mock_asset_repo,
|
|
current_user=_make_auth_user(),
|
|
)
|
|
|
|
assert result.created_count == 1
|