f10fd9cd5c
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) 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 Worker Image (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 (pull_request) Successful in 4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
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 - Style (pull_request) 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 / 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 API Image (pull_request) Successful in 8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 35s
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 Worker Image (pull_request) Successful in 40s
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 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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m11s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m41s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
AI Code Review / AI Code Review (pull_request) Successful in 6m28s
CI/CD Pipeline / Validate - Security (push) Successful in 7m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m40s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 7m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m22s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m29s
CI/CD Pipeline / Unit Tests (push) Successful in 13m51s
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>
487 lines
18 KiB
Python
Executable File
487 lines
18 KiB
Python
Executable File
"""测试:渲染完成后 generated_videos 写表判断逻辑.
|
|
|
|
验证:project_id 为空时,只要有 generation_task_id 就应该写入 generated_videos。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
|
|
SQLAlchemyGenerationTaskRepository,
|
|
)
|
|
from packages.adapters.sqlalchemy_impl.models import Base
|
|
from packages.domain import GenerationTask, GenerationTaskStatus
|
|
|
|
|
|
def _make_task(**kwargs) -> GenerationTask:
|
|
defaults = dict(
|
|
project_id="proj-1",
|
|
asset_library_id="lib-1",
|
|
created_by_user_id="user-1",
|
|
status=GenerationTaskStatus.PENDING,
|
|
progress=0.0,
|
|
source_edit_plan_id="plan-1",
|
|
)
|
|
defaults.update(kwargs)
|
|
return GenerationTask(id="task-" + kwargs.get("project_id", "t1")[:8], **defaults)
|
|
|
|
|
|
class TestCleanupStaleRunning:
|
|
"""复用已有的 repo 测试基础设施,验证写表判断逻辑。"""
|
|
|
|
def _setup(self):
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
return SQLAlchemyGenerationTaskRepository(session), session
|
|
|
|
def test_generation_task_without_project_id_can_be_created(self):
|
|
"""验证:project_id 为空的 generation_task 可以正常创建(模拟一键生成场景)。"""
|
|
repo, session = self._setup()
|
|
try:
|
|
task = _make_task(
|
|
project_id="",
|
|
status=GenerationTaskStatus.COMPLETED,
|
|
progress=100.0,
|
|
result_count=1,
|
|
source_edit_plan_id="plan-empty-proj",
|
|
)
|
|
task.id = "task-empty-proj"
|
|
created = repo.create(task)
|
|
assert created.project_id == ""
|
|
|
|
fetched = repo.get("task-empty-proj")
|
|
assert fetched is not None
|
|
assert fetched.project_id == ""
|
|
assert fetched.status.value == "completed"
|
|
assert fetched.progress == 100.0
|
|
finally:
|
|
session.close()
|
|
|
|
def test_generation_task_with_project_id_can_be_created(self):
|
|
"""验证:project_id 有值的 generation_task 正常(回归测试)。"""
|
|
repo, session = self._setup()
|
|
try:
|
|
task = _make_task(
|
|
project_id="proj-123",
|
|
status=GenerationTaskStatus.COMPLETED,
|
|
progress=100.0,
|
|
result_count=1,
|
|
)
|
|
task.id = "task-with-proj"
|
|
created = repo.create(task)
|
|
assert created.project_id == "proj-123"
|
|
|
|
fetched = repo.get("task-with-proj")
|
|
assert fetched is not None
|
|
assert fetched.project_id == "proj-123"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
class TestVideoCreationLogic:
|
|
"""验证写表判断逻辑(纯逻辑测试,不依赖 DB)。"""
|
|
|
|
def test_should_create_video_with_generation_task_id_and_empty_project(self):
|
|
"""有 generation_task_id、无 project_id → 应该创建视频(修复点)。"""
|
|
generation_task_id = "task-123"
|
|
project_id = ""
|
|
# 修复后:只要有 generation_task_id 就创建
|
|
assert bool(generation_task_id) is True
|
|
# 之前的错误逻辑:if generation_task_id and project_id → False
|
|
assert bool(generation_task_id and project_id) is False
|
|
# 修复后的逻辑:if generation_task_id → True
|
|
assert bool(generation_task_id) is True
|
|
|
|
def test_should_create_video_with_both_ids(self):
|
|
"""有 generation_task_id、有 project_id → 应该创建视频(回归)。"""
|
|
generation_task_id = "task-123"
|
|
project_id = "proj-456"
|
|
assert bool(generation_task_id) is True
|
|
assert bool(generation_task_id and project_id) is True
|
|
|
|
def test_should_not_create_video_without_generation_task_id(self):
|
|
"""无 generation_task_id → 不创建视频(边界)。"""
|
|
generation_task_id = ""
|
|
project_id = "proj-456"
|
|
assert bool(generation_task_id) is False
|
|
|
|
|
|
class TestVideoNameParameter:
|
|
"""验证成片库视频名称逻辑:用户设置标题时用标题,没设置时用默认命名。"""
|
|
|
|
def test_name_from_user_title(self):
|
|
"""用户设置了标题 → 用标题作为视频名称。"""
|
|
# 验证函数签名包含 name 参数
|
|
import inspect
|
|
|
|
from video_processing.dedup_helpers import create_video_record_and_dedup
|
|
|
|
sig = inspect.signature(create_video_record_and_dedup)
|
|
assert "name" in sig.parameters, "create_video_record_and_dedup 应支持 name 参数"
|
|
|
|
def test_generated_video_requires_name(self):
|
|
"""GeneratedVideo.create 要求 name 非空。"""
|
|
from packages.domain import GeneratedVideo
|
|
|
|
with pytest.raises(ValueError, match="name cannot be empty"):
|
|
GeneratedVideo.create(
|
|
project_id="proj-1",
|
|
user_id="user-1",
|
|
generation_task_id="task-1",
|
|
name="",
|
|
file_url="https://example.com/test.mp4",
|
|
)
|
|
|
|
def test_name_fallback_when_empty(self):
|
|
"""name 为空或纯空格时,调用方应 fallback 到默认命名。"""
|
|
# 模拟 _finalize_render_success 中的逻辑
|
|
title_text_empty = ""
|
|
title_text_spaces = " "
|
|
generation_task_id = "task-abcdef123456"
|
|
|
|
# 空标题 → fallback
|
|
video_name_1 = title_text_empty.strip() or f"generated-{generation_task_id[:8]}.mp4"
|
|
assert video_name_1 == f"generated-{generation_task_id[:8]}.mp4"
|
|
|
|
# 纯空格 → fallback
|
|
video_name_2 = title_text_spaces.strip() or f"generated-{generation_task_id[:8]}.mp4"
|
|
assert video_name_2 == f"generated-{generation_task_id[:8]}.mp4"
|
|
|
|
# 有标题 → 用标题
|
|
title_text = "我的旅行vlog"
|
|
video_name_3 = title_text.strip() or f"generated-{generation_task_id[:8]}.mp4"
|
|
assert video_name_3 == "我的旅行vlog"
|
|
|
|
|
|
class TestUserIdFilter:
|
|
"""验证成片库按 user_id 过滤的核心逻辑。"""
|
|
|
|
def _setup_repo(self):
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.generated_video_repository import (
|
|
SQLAlchemyGeneratedVideoRepository,
|
|
)
|
|
from packages.adapters.sqlalchemy_impl.models import Base
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
session = sessionmaker(bind=engine)()
|
|
return SQLAlchemyGeneratedVideoRepository(session), session
|
|
|
|
def test_list_paginated_filters_by_user_id(self):
|
|
"""list_paginated 传入 user_id 时只返回该用户的视频。"""
|
|
from packages.domain import GeneratedVideo
|
|
|
|
repo, session = self._setup_repo()
|
|
try:
|
|
# 用户A的2个视频
|
|
for i in range(2):
|
|
v = GeneratedVideo.create(
|
|
project_id=f"proj-a-{i}",
|
|
user_id="user-a",
|
|
generation_task_id=f"task-a-{i}",
|
|
name=f"video-a-{i}.mp4",
|
|
file_url=f"https://oss.example.com/a-{i}.mp4",
|
|
)
|
|
repo.create(v)
|
|
|
|
# 用户B的3个视频
|
|
for i in range(3):
|
|
v = GeneratedVideo.create(
|
|
project_id=f"proj-b-{i}",
|
|
user_id="user-b",
|
|
generation_task_id=f"task-b-{i}",
|
|
name=f"video-b-{i}.mp4",
|
|
file_url=f"https://oss.example.com/b-{i}.mp4",
|
|
)
|
|
repo.create(v)
|
|
|
|
# 查用户A → 2条
|
|
items, total = repo.list_paginated(user_id="user-a", page=1, page_size=10)
|
|
assert total == 2
|
|
assert len(items) == 2
|
|
assert all(it.user_id == "user-a" for it in items)
|
|
|
|
# 查用户B → 3条
|
|
items, total = repo.list_paginated(user_id="user-b", page=1, page_size=10)
|
|
assert total == 3
|
|
assert len(items) == 3
|
|
assert all(it.user_id == "user-b" for it in items)
|
|
finally:
|
|
session.close()
|
|
|
|
def test_list_paginated_user_id_plus_project_id(self):
|
|
"""同时传 user_id 和 project_id 时两个条件同时过滤。"""
|
|
from packages.domain import GeneratedVideo
|
|
|
|
repo, session = self._setup_repo()
|
|
try:
|
|
# 用户A的proj-1视频
|
|
v1 = GeneratedVideo.create(
|
|
project_id="proj-1",
|
|
user_id="user-a",
|
|
generation_task_id="task-1",
|
|
name="v1.mp4",
|
|
file_url="https://oss.example.com/v1.mp4",
|
|
)
|
|
repo.create(v1)
|
|
|
|
# 用户B的proj-1视频(不同用户同项目)
|
|
v2 = GeneratedVideo.create(
|
|
project_id="proj-1",
|
|
user_id="user-b",
|
|
generation_task_id="task-2",
|
|
name="v2.mp4",
|
|
file_url="https://oss.example.com/v2.mp4",
|
|
)
|
|
repo.create(v2)
|
|
|
|
# 用户A + proj-1 → 只有1条
|
|
items, total = repo.list_paginated(user_id="user-a", project_id="proj-1", page=1, page_size=10)
|
|
assert total == 1
|
|
assert items[0].user_id == "user-a"
|
|
assert items[0].generation_task_id == "task-1"
|
|
finally:
|
|
session.close()
|
|
|
|
def test_generated_video_has_user_id_field(self):
|
|
"""GeneratedVideo domain 对象有 user_id 字段。"""
|
|
from packages.domain import GeneratedVideo
|
|
|
|
v = GeneratedVideo.create(
|
|
project_id="proj-1",
|
|
user_id="user-123",
|
|
generation_task_id="task-1",
|
|
name="test.mp4",
|
|
file_url="https://example.com/test.mp4",
|
|
)
|
|
assert v.user_id == "user-123"
|
|
|
|
def test_created_video_persists_user_id(self):
|
|
"""创建视频后 user_id 能正确持久化和读取。"""
|
|
from packages.domain import GeneratedVideo
|
|
|
|
repo, session = self._setup_repo()
|
|
try:
|
|
v = GeneratedVideo.create(
|
|
project_id="proj-1",
|
|
user_id="user-persist-test",
|
|
generation_task_id="task-persist",
|
|
name="persist.mp4",
|
|
file_url="https://example.com/persist.mp4",
|
|
)
|
|
repo.create(v)
|
|
|
|
fetched = repo.get(v.id)
|
|
assert fetched is not None
|
|
assert fetched.user_id == "user-persist-test"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
class TestThumbnailInDedupHelpers:
|
|
"""验证 dedup_helpers 中缩略图相关逻辑(全 mock,不依赖 cv2)。"""
|
|
|
|
@classmethod
|
|
def setup_class(cls):
|
|
"""用 mock 模块替代需要 cv2 的 dedup 模块,避免导入失败。
|
|
|
|
注意:光往 sys.modules 塞不够,patch() 走属性访问链,
|
|
必须给 video_processing 包设置对应子模块属性。
|
|
"""
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
# 先 mock 掉 cv2
|
|
if "cv2" not in sys.modules:
|
|
sys.modules["cv2"] = MagicMock()
|
|
|
|
# mock video_processing.dedup
|
|
mock_dedup = MagicMock()
|
|
mock_dedup.VideoDeduplicator = MagicMock()
|
|
sys.modules["video_processing.dedup"] = mock_dedup
|
|
|
|
# mock video_processing.thumbnail_generator (统一封面管道: 仅保留 extract_first_frame)
|
|
mock_thumb = MagicMock()
|
|
mock_thumb.extract_first_frame = MagicMock()
|
|
sys.modules["video_processing.thumbnail_generator"] = mock_thumb
|
|
|
|
# 关键:给 video_processing 包设置子模块属性,让 patch() 能通过属性访问找到
|
|
import video_processing
|
|
|
|
video_processing.dedup = mock_dedup
|
|
video_processing.thumbnail_generator = mock_thumb
|
|
|
|
@classmethod
|
|
def teardown_class(cls):
|
|
"""恢复 setup_class 中替换的模块,避免污染后续测试。"""
|
|
import sys
|
|
|
|
import video_processing
|
|
|
|
# 从 sys.modules 移除 mock 模块
|
|
for mod_name in ("video_processing.thumbnail_generator", "video_processing.dedup"):
|
|
sys.modules.pop(mod_name, None)
|
|
|
|
# 重新导入真实模块以恢复 sys.modules
|
|
try:
|
|
import video_processing.thumbnail_generator # noqa: F401
|
|
except Exception:
|
|
pass
|
|
try:
|
|
import video_processing.dedup # noqa: F401
|
|
except Exception:
|
|
pass
|
|
|
|
def test_pre_generated_thumbnail_url_is_reused(self):
|
|
"""传入 thumbnail_url 时直接复用,统一封面管道不再自动生成缩略图。"""
|
|
from video_processing.dedup_helpers import create_video_record_and_dedup
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
pre_thumb_url = "https://oss.example.com/pre-thumb.jpg"
|
|
|
|
try:
|
|
with patch("video_processing.dedup.VideoDeduplicator") as mock_dedup_cls:
|
|
mock_dedup = mock_dedup_cls.return_value
|
|
mock_dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
|
|
mock_dedup.check_duplicate.return_value = None
|
|
mock_dedup.check_batch_duplicate.return_value = None
|
|
mock_dedup.compute_duplicate_rate.return_value = {
|
|
"duplicate_rate": 0.0,
|
|
"visual_similarity": 0.0,
|
|
"match_count": 0,
|
|
}
|
|
|
|
result = create_video_record_and_dedup(
|
|
generation_task_id="task-thumb-reuse",
|
|
project_id="proj-1",
|
|
batch_id="",
|
|
file_url="https://oss.example.com/video.mp4",
|
|
file_size=1024,
|
|
duration=10.0,
|
|
video_path="/tmp/fake.mp4",
|
|
mode="edit_plan",
|
|
session=session,
|
|
width=1280,
|
|
height=720,
|
|
fps=25.0,
|
|
thumbnail_url=pre_thumb_url,
|
|
)
|
|
|
|
assert result == 1
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel
|
|
|
|
video = session.query(GeneratedVideoModel).filter_by(generation_task_id="task-thumb-reuse").first()
|
|
assert video is not None
|
|
assert video.thumbnail_url == pre_thumb_url
|
|
finally:
|
|
session.close()
|
|
|
|
def test_no_thumbnail_when_not_provided(self):
|
|
"""未传 thumbnail_url 时不生成缩略图(统一封面管道已移除自动缩略图生成)。"""
|
|
from video_processing.dedup_helpers import create_video_record_and_dedup
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
try:
|
|
with patch("video_processing.dedup.VideoDeduplicator") as mock_dedup_cls:
|
|
mock_dedup = mock_dedup_cls.return_value
|
|
mock_dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
|
|
mock_dedup.check_duplicate.return_value = None
|
|
mock_dedup.check_batch_duplicate.return_value = None
|
|
mock_dedup.compute_duplicate_rate.return_value = {
|
|
"duplicate_rate": 0.0,
|
|
"visual_similarity": 0.0,
|
|
"match_count": 0,
|
|
}
|
|
|
|
result = create_video_record_and_dedup(
|
|
generation_task_id="task-thumb-gen",
|
|
project_id="proj-1",
|
|
batch_id="",
|
|
file_url="https://oss.example.com/video.mp4",
|
|
file_size=1024,
|
|
duration=10.0,
|
|
video_path="/tmp/fake.mp4",
|
|
mode="edit_plan",
|
|
session=session,
|
|
width=1280,
|
|
height=720,
|
|
fps=25.0,
|
|
)
|
|
|
|
assert result == 1
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel
|
|
|
|
video = session.query(GeneratedVideoModel).filter_by(generation_task_id="task-thumb-gen").first()
|
|
assert video is not None
|
|
# 统一封面管道下,不传 thumbnail_url 时不自动生成
|
|
assert not video.thumbnail_url
|
|
finally:
|
|
session.close()
|
|
|
|
def test_no_thumbnail_does_not_block(self):
|
|
"""统一封面管道下,缩略图不再在 dedup 阶段生成。"""
|
|
from video_processing.dedup_helpers import create_video_record_and_dedup
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
try:
|
|
with patch("video_processing.dedup.VideoDeduplicator") as mock_dedup_cls:
|
|
mock_dedup = mock_dedup_cls.return_value
|
|
mock_dedup.compute_fingerprint.return_value = MagicMock(to_dict=lambda: {})
|
|
mock_dedup.check_duplicate.return_value = None
|
|
mock_dedup.check_batch_duplicate.return_value = None
|
|
mock_dedup.compute_duplicate_rate.return_value = {
|
|
"duplicate_rate": 0.0,
|
|
"visual_similarity": 0.0,
|
|
"match_count": 0,
|
|
}
|
|
|
|
result = create_video_record_and_dedup(
|
|
generation_task_id="task-thumb-fail",
|
|
project_id="proj-1",
|
|
batch_id="",
|
|
file_url="https://oss.example.com/video.mp4",
|
|
file_size=1024,
|
|
duration=10.0,
|
|
video_path="/tmp/fake.mp4",
|
|
mode="edit_plan",
|
|
session=session,
|
|
width=1280,
|
|
height=720,
|
|
fps=25.0,
|
|
)
|
|
|
|
assert result == 1 # 不阻断
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel
|
|
|
|
video = session.query(GeneratedVideoModel).filter_by(generation_task_id="task-thumb-fail").first()
|
|
assert video is not None
|
|
# 缩略图生成失败时 thumbnail_url 为 None 或空串
|
|
assert not video.thumbnail_url
|
|
finally:
|
|
session.close()
|