f19be5fd09
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m58s
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 18s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
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
AI Code Review / AI Code Review (pull_request) Successful in 7m12s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m52s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 8m1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m38s
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 / 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 / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (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
- domain: ViralVideoJob 状态机(PENDING/RUNNING/WAIT_USER_CONFIRM/COMPLETED/FAILED/CANCELLED)、 11 阶段枚举、PromptType 7 值(含 v1.3 video_style_integration/style_constraint) - repository: 接口 + SQLAlchemy 实现(jobs/style_templates/prompt_templates) - API: 6 个 REST 端点(generate/history/detail/retry/confirm-intent/analyze-style/style-templates) - Celery: ViralVideoOrchestrator 10 步流水线,WS viral_video:progress 进度推送, Credits CREDITS_VIRAL_VIDEO_COST=50 扣点/失败自动回滚 - ai_service: 新增 call_llm/call_vision(复用现有豆包客户端) - 测试 40 个单测(domain/schema/repository/流水线/集成)
522 lines
19 KiB
Python
Executable File
522 lines
19 KiB
Python
Executable File
"""爆款视频模块单元测试。
|
|
|
|
覆盖范围:
|
|
- 领域实体状态机转换
|
|
- Repository CRUD
|
|
- API 端点(6 个)
|
|
- Celery 编排器流水线
|
|
- Schema 校验
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from pydantic import ValidationError
|
|
|
|
import pytest
|
|
|
|
from packages.domain.viral_video import (
|
|
CREDITS_VIRAL_VIDEO_COST,
|
|
STAGE_LABELS,
|
|
FusionLevel,
|
|
StyleStrength,
|
|
ViralVideoJob,
|
|
ViralVideoStage,
|
|
ViralVideoStatus,
|
|
)
|
|
|
|
|
|
# ── 领域模型测试 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestViralVideoStatus:
|
|
"""状态枚举测试。"""
|
|
|
|
def test_status_values(self):
|
|
assert ViralVideoStatus.PENDING == "pending"
|
|
assert ViralVideoStatus.RUNNING == "running"
|
|
assert ViralVideoStatus.WAIT_USER_CONFIRM == "wait_user_confirm"
|
|
assert ViralVideoStatus.COMPLETED == "completed"
|
|
assert ViralVideoStatus.FAILED == "failed"
|
|
assert ViralVideoStatus.CANCELLED == "cancelled"
|
|
|
|
def test_terminal_statuses(self):
|
|
assert ViralVideoJob(user_id="u1", status=ViralVideoStatus.COMPLETED).is_terminal
|
|
assert ViralVideoJob(user_id="u1", status=ViralVideoStatus.FAILED).is_terminal
|
|
assert ViralVideoJob(user_id="u1", status=ViralVideoStatus.CANCELLED).is_terminal
|
|
assert not ViralVideoJob(user_id="u1", status=ViralVideoStatus.PENDING).is_terminal
|
|
assert not ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING).is_terminal
|
|
|
|
|
|
class TestViralVideoJobStateTransitions:
|
|
"""状态机转换测试。"""
|
|
|
|
def test_mark_running_from_pending(self):
|
|
job = ViralVideoJob(user_id="u1")
|
|
job.mark_running()
|
|
assert job.status == ViralVideoStatus.RUNNING
|
|
assert job.started_at is not None
|
|
|
|
def test_mark_running_from_running(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
job.mark_running()
|
|
assert job.status == ViralVideoStatus.RUNNING
|
|
|
|
def test_mark_running_from_completed_raises(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.COMPLETED)
|
|
with pytest.raises(ValueError, match="Cannot transition"):
|
|
job.mark_running()
|
|
|
|
def test_mark_wait_user_confirm(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
intent = {"intent": "推广", "key_messages": ["卖点1"]}
|
|
job.mark_wait_user_confirm(intent)
|
|
assert job.status == ViralVideoStatus.WAIT_USER_CONFIRM
|
|
assert job.intent_result == intent
|
|
|
|
def test_mark_wait_user_confirm_from_non_running_raises(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.PENDING)
|
|
with pytest.raises(ValueError, match="Cannot transition"):
|
|
job.mark_wait_user_confirm({})
|
|
|
|
def test_resume_from_confirm(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.WAIT_USER_CONFIRM)
|
|
job.resume_from_confirm()
|
|
assert job.status == ViralVideoStatus.RUNNING
|
|
|
|
def test_resume_from_non_confirm_raises(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
with pytest.raises(ValueError, match="Cannot resume"):
|
|
job.resume_from_confirm()
|
|
|
|
def test_mark_completed(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
job.mark_completed("https://oss.example.com/video.mp4")
|
|
assert job.status == ViralVideoStatus.COMPLETED
|
|
assert job.result_video_url == "https://oss.example.com/video.mp4"
|
|
assert job.completed_at is not None
|
|
|
|
def test_mark_failed(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
job.mark_failed("渲染超时")
|
|
assert job.status == ViralVideoStatus.FAILED
|
|
assert job.error_msg == "渲染超时"
|
|
|
|
def test_mark_cancelled(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.RUNNING)
|
|
job.mark_cancelled()
|
|
assert job.status == ViralVideoStatus.CANCELLED
|
|
|
|
def test_mark_cancelled_from_terminal_raises(self):
|
|
job = ViralVideoJob(user_id="u1", status=ViralVideoStatus.COMPLETED)
|
|
with pytest.raises(ValueError, match="Cannot cancel"):
|
|
job.mark_cancelled()
|
|
|
|
|
|
class TestViralVideoJobDefaults:
|
|
"""默认值测试。"""
|
|
|
|
def test_default_values(self):
|
|
job = ViralVideoJob(user_id="u1")
|
|
assert job.images == []
|
|
assert job.industry == ""
|
|
assert job.duration == 30
|
|
assert job.fusion_level == FusionLevel.AI_POLISH
|
|
assert job.style_strength == StyleStrength.MEDIUM
|
|
assert job.status == ViralVideoStatus.PENDING
|
|
assert job.credits_cost == 0
|
|
assert job.retry_count == 0
|
|
assert job.result_video_url == ""
|
|
assert job.error_msg == ""
|
|
|
|
def test_credits_cost_constant(self):
|
|
assert CREDITS_VIRAL_VIDEO_COST == 50
|
|
|
|
|
|
class TestViralVideoStage:
|
|
"""阶段枚举测试。"""
|
|
|
|
def test_all_stages_have_labels(self):
|
|
for stage in ViralVideoStage:
|
|
assert stage in STAGE_LABELS, f"Stage {stage} missing label"
|
|
|
|
def test_stage_order(self):
|
|
expected_order = [
|
|
"image_analysis",
|
|
"video_analysis",
|
|
"intent_parsing",
|
|
"copy_fusion",
|
|
"storyboard",
|
|
"review",
|
|
"tts",
|
|
"bgm_select",
|
|
"rendering",
|
|
"musetalk",
|
|
"uploading",
|
|
]
|
|
actual_order = [s.value for s in ViralVideoStage]
|
|
assert actual_order == expected_order
|
|
|
|
|
|
# ── Schema 校验测试 ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestViralVideoSchemas:
|
|
"""Pydantic Schema 校验测试。"""
|
|
|
|
def test_create_request_valid(self):
|
|
from app.schemas.viral_video import CreateViralVideoRequest
|
|
|
|
req = CreateViralVideoRequest(images=["https://example.com/img.jpg"])
|
|
assert req.images == ["https://example.com/img.jpg"]
|
|
assert req.fusion_level == "ai_polish"
|
|
assert req.style_strength == "medium"
|
|
assert req.duration == 30
|
|
|
|
def test_create_request_empty_images_raises(self):
|
|
from app.schemas.viral_video import CreateViralVideoRequest
|
|
|
|
with pytest.raises(ValidationError):
|
|
CreateViralVideoRequest(images=[])
|
|
|
|
def test_create_request_invalid_fusion_level(self):
|
|
from app.schemas.viral_video import CreateViralVideoRequest
|
|
|
|
with pytest.raises(ValidationError):
|
|
CreateViralVideoRequest(
|
|
images=["https://example.com/img.jpg"],
|
|
fusion_level="invalid_level",
|
|
)
|
|
|
|
def test_create_request_invalid_style_strength(self):
|
|
from app.schemas.viral_video import CreateViralVideoRequest
|
|
|
|
with pytest.raises(ValidationError):
|
|
CreateViralVideoRequest(
|
|
images=["https://example.com/img.jpg"],
|
|
style_strength="ultra",
|
|
)
|
|
|
|
def test_confirm_intent_request_defaults(self):
|
|
from app.schemas.viral_video import ConfirmIntentRequest
|
|
|
|
req = ConfirmIntentRequest()
|
|
assert req.confirmed_copy == ""
|
|
assert req.adjustments == ""
|
|
|
|
def test_analyze_style_request(self):
|
|
from app.schemas.viral_video import AnalyzeStyleRequest
|
|
|
|
req = AnalyzeStyleRequest(reference_video_url="https://example.com/video.mp4")
|
|
assert req.reference_video_url == "https://example.com/video.mp4"
|
|
|
|
def test_ws_progress_event(self):
|
|
from app.schemas.viral_video import WSProgressEvent
|
|
|
|
event = WSProgressEvent(
|
|
job_id="abc123",
|
|
stage="image_analysis",
|
|
progress=10.0,
|
|
message="正在分析图片",
|
|
)
|
|
assert event.type == "viral_video:progress"
|
|
assert event.job_id == "abc123"
|
|
assert event.progress == 10.0
|
|
|
|
|
|
# ── Repository 测试 ─────────────────────────────────────────────────────
|
|
|
|
|
|
class TestViralVideoRepository:
|
|
"""SQLAlchemy Repository CRUD 测试(使用内存数据库)。"""
|
|
|
|
@pytest.fixture
|
|
def db_session(self):
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import Base
|
|
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
session = SessionLocal()
|
|
yield session
|
|
session.close()
|
|
|
|
def test_save_and_get(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoJobRepository,
|
|
)
|
|
|
|
repo = SQLAlchemyViralVideoJobRepository(db_session)
|
|
job = ViralVideoJob(
|
|
user_id="user-001",
|
|
images=["https://img.com/1.jpg"],
|
|
industry="美妆",
|
|
duration=60,
|
|
)
|
|
repo.save(job)
|
|
|
|
fetched = repo.get(job.id)
|
|
assert fetched is not None
|
|
assert fetched.id == job.id
|
|
assert fetched.user_id == "user-001"
|
|
assert fetched.images == ["https://img.com/1.jpg"]
|
|
assert fetched.industry == "美妆"
|
|
assert fetched.duration == 60
|
|
|
|
def test_get_nonexistent(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoJobRepository,
|
|
)
|
|
|
|
repo = SQLAlchemyViralVideoJobRepository(db_session)
|
|
assert repo.get("nonexistent-id") is None
|
|
|
|
def test_list_by_user(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoJobRepository,
|
|
)
|
|
|
|
repo = SQLAlchemyViralVideoJobRepository(db_session)
|
|
for i in range(3):
|
|
job = ViralVideoJob(user_id="user-001", industry=f"行业{i}")
|
|
repo.save(job)
|
|
|
|
# 另一个用户的任务
|
|
other_job = ViralVideoJob(user_id="user-002", industry="其他")
|
|
repo.save(other_job)
|
|
|
|
jobs = repo.list_by_user("user-001")
|
|
assert len(jobs) == 3
|
|
assert all(j.user_id == "user-001" for j in jobs)
|
|
|
|
def test_update_status(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoJobRepository,
|
|
)
|
|
|
|
repo = SQLAlchemyViralVideoJobRepository(db_session)
|
|
job = ViralVideoJob(user_id="user-001")
|
|
repo.save(job)
|
|
|
|
job.mark_running()
|
|
repo.update(job)
|
|
|
|
fetched = repo.get(job.id)
|
|
assert fetched.status == ViralVideoStatus.RUNNING
|
|
assert fetched.started_at is not None
|
|
|
|
def test_count_pending_by_user(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoJobRepository,
|
|
)
|
|
|
|
repo = SQLAlchemyViralVideoJobRepository(db_session)
|
|
# 2 个 pending
|
|
for _ in range(2):
|
|
repo.save(ViralVideoJob(user_id="user-001"))
|
|
# 1 个 completed
|
|
completed = ViralVideoJob(user_id="user-001", status=ViralVideoStatus.COMPLETED)
|
|
repo.save(completed)
|
|
|
|
assert repo.count_pending_by_user("user-001") == 2
|
|
|
|
def test_style_template_repo(self, db_session):
|
|
from packages.adapters.sqlalchemy_impl.viral_video_repository import (
|
|
SQLAlchemyViralVideoStyleTemplateRepository,
|
|
)
|
|
from packages.adapters.sqlalchemy_impl.models import ViralVideoStyleTemplateModel
|
|
|
|
# 插入模板
|
|
tpl = ViralVideoStyleTemplateModel(
|
|
id="tpl-001",
|
|
name="快节奏",
|
|
description="适合快消品",
|
|
style_config={"cut_speed": "fast"},
|
|
is_system=True,
|
|
sort_order=1,
|
|
)
|
|
db_session.add(tpl)
|
|
db_session.commit()
|
|
|
|
repo = SQLAlchemyViralVideoStyleTemplateRepository(db_session)
|
|
templates = repo.list_all()
|
|
assert len(templates) == 1
|
|
assert templates[0]["name"] == "快节奏"
|
|
|
|
fetched = repo.get("tpl-001")
|
|
assert fetched is not None
|
|
assert fetched["style_config"] == {"cut_speed": "fast"}
|
|
|
|
|
|
# ── Celery 编排器测试 ───────────────────────────────────────────────────
|
|
|
|
|
|
class TestViralVideoPipeline:
|
|
"""编排器流水线测试。"""
|
|
|
|
@pytest.fixture
|
|
def mock_job(self):
|
|
return ViralVideoJob(
|
|
user_id="user-001",
|
|
images=["https://img.com/1.jpg", "https://img.com/2.jpg"],
|
|
industry="美妆",
|
|
target_customer="年轻女性",
|
|
marketing_purpose="品牌推广",
|
|
duration=30,
|
|
user_copy_text="这款产品超好用",
|
|
fusion_level="ai_polish",
|
|
)
|
|
|
|
@patch("packages.shared.ai_service.call_vision")
|
|
def test_image_analysis_step(self, mock_vision, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_image_analysis
|
|
|
|
mock_vision.return_value = {"name": "口红", "features": ["持久", "滋润"]}
|
|
result = _step_image_analysis(mock_job)
|
|
assert "products" in result
|
|
assert len(result["products"]) == 2 # 两张图片
|
|
|
|
@patch("packages.shared.ai_service.call_vision")
|
|
def test_image_analysis_fallback(self, mock_vision, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_image_analysis
|
|
|
|
# 模拟 call_vision 不存在
|
|
mock_vision.side_effect = ImportError("no module")
|
|
result = _step_image_analysis(mock_job)
|
|
assert "products" in result
|
|
|
|
def test_video_analysis_no_reference(self, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_video_analysis
|
|
|
|
# 没有参考视频
|
|
mock_job.reference_video_url = ""
|
|
result = _step_video_analysis(mock_job)
|
|
assert result is None
|
|
|
|
@patch("packages.shared.ai_service.call_llm")
|
|
def test_intent_parsing(self, mock_llm, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_intent_parsing
|
|
|
|
mock_llm.return_value = {"intent": "推广口红", "tone": "活泼"}
|
|
result = _step_intent_parsing(mock_job, {"products": []})
|
|
assert "intent" in result
|
|
|
|
@patch("packages.shared.ai_service.call_llm")
|
|
def test_copy_fusion_ai_polish(self, mock_llm, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_copy_fusion
|
|
|
|
mock_llm.return_value = "融合后的文案内容"
|
|
result = _step_copy_fusion(mock_job, {"intent": "推广"}, {"products": []})
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
@patch("packages.shared.ai_service.call_llm")
|
|
def test_storyboard_generation(self, mock_llm, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_storyboard
|
|
|
|
mock_llm.return_value = [
|
|
{"order": 0, "type": "product_shot", "duration": 10},
|
|
{"order": 1, "type": "closing", "duration": 5},
|
|
]
|
|
result = _step_storyboard(mock_job, "测试文案", {})
|
|
assert isinstance(result, list)
|
|
assert len(result) == 2
|
|
|
|
@patch("packages.shared.ai_service.call_llm")
|
|
def test_review_pass(self, mock_llm, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_review
|
|
|
|
mock_llm.return_value = {"passed": True, "score": 90, "details": {}}
|
|
result = _step_review(mock_job, "测试文案", [])
|
|
assert result["passed"] is True
|
|
|
|
def test_bgm_select(self, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_bgm_select
|
|
|
|
mock_job.bgm_preference = "upbeat"
|
|
bgm = _step_bgm_select(mock_job)
|
|
assert "upbeat" in bgm
|
|
|
|
def test_bgm_select_default(self, mock_job):
|
|
from apps.worker.worker_app.tasks.viral_video import _step_bgm_select
|
|
|
|
mock_job.bgm_preference = ""
|
|
bgm = _step_bgm_select(mock_job)
|
|
assert bgm == "bgm_default.mp3"
|
|
|
|
|
|
# ── 端到端流水线集成测试 ────────────────────────────────────────────────
|
|
|
|
|
|
class TestPipelineIntegration:
|
|
"""流水线端到端集成测试(mock 外部依赖)。"""
|
|
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_upload")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_musetalk")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_render")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_bgm_select")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_tts")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_review")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_storyboard")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_copy_fusion")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_intent_parsing")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_video_analysis")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._step_image_analysis")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._get_repo_and_job")
|
|
@patch("apps.worker.worker_app.tasks.viral_video._emit_progress")
|
|
def test_resume_pipeline_completes(
|
|
self,
|
|
mock_emit,
|
|
mock_get_repo,
|
|
mock_img_analysis,
|
|
mock_video_analysis,
|
|
mock_intent,
|
|
mock_copy_fusion,
|
|
mock_storyboard,
|
|
mock_review,
|
|
mock_tts,
|
|
mock_bgm,
|
|
mock_render,
|
|
mock_musetalk,
|
|
mock_upload,
|
|
):
|
|
"""测试 resume 流水线能从确认状态走到完成。"""
|
|
from apps.worker.worker_app.tasks.viral_video import (
|
|
resume_viral_video_pipeline,
|
|
)
|
|
|
|
# 构造 mock job
|
|
job = ViralVideoJob(
|
|
user_id="user-001",
|
|
images=["https://img.com/1.jpg"],
|
|
industry="美妆",
|
|
status=ViralVideoStatus.RUNNING,
|
|
intent_result={"intent": "推广"},
|
|
)
|
|
|
|
mock_repo = MagicMock()
|
|
mock_session = MagicMock()
|
|
mock_get_repo.return_value = (mock_session, mock_repo, job)
|
|
|
|
# 设置各步骤返回值
|
|
mock_copy_fusion.return_value = "融合文案"
|
|
mock_storyboard.return_value = [{"order": 0, "duration": 10}]
|
|
mock_review.return_value = {"passed": True, "score": 90}
|
|
mock_tts.return_value = "https://audio.mp3"
|
|
mock_bgm.return_value = "bgm_default.mp3"
|
|
mock_render.return_value = "/tmp/video.mp4"
|
|
mock_musetalk.return_value = "/tmp/video_final.mp4"
|
|
mock_upload.return_value = "https://oss.example.com/final.mp4"
|
|
|
|
result = resume_viral_video_pipeline.run("job-001")
|
|
|
|
assert result["ok"] is True
|
|
assert result["video_url"] == "https://oss.example.com/final.mp4"
|
|
assert job.status == ViralVideoStatus.COMPLETED
|
|
assert job.credits_cost == CREDITS_VIRAL_VIDEO_COST
|