981d965d89
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (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 / Build Staging Web Image (push) Successful in 48s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m18s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 5m42s
CI/CD Pipeline / Unit Tests (push) Successful in 6m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m13s
CI/CD Pipeline / Integration Tests (push) Successful in 2m14s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m6s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 17m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
268 lines
9.7 KiB
Python
268 lines
9.7 KiB
Python
"""config_schemas + AI 推荐/封面生成 单元测试.
|
||
|
||
覆盖(20+ 测试用例):
|
||
- config_schemas: normalize_plan_config / normalize_template_config 默认值填充、部分覆盖、非标准字段保留
|
||
- config_schemas: Pydantic 枚举校验(CoverType / TextPosition / BGMSource)
|
||
- ai_tasks: run_ai_recommend / run_generate_cover stub 返回结构
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
|
||
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
|
||
|
||
import pytest
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# config_schemas 单元测试
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestNormalizePlanConfig:
|
||
def test_none_returns_full_defaults(self):
|
||
from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG, normalize_plan_config
|
||
|
||
result = normalize_plan_config(None)
|
||
assert result == DEFAULT_EDIT_PLAN_CONFIG.copy()
|
||
|
||
def test_empty_dict_returns_full_defaults(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config({})
|
||
assert result["cover"]["type"] == "ai_frame"
|
||
assert result["title"]["enabled"] is True
|
||
assert result["subtitle"]["position"] == "bottom"
|
||
assert result["bgm"]["volume"] == 0.3
|
||
|
||
def test_partial_cover_override(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config({"cover": {"type": "upload", "image_url": "https://example.com/cover.jpg"}})
|
||
assert result["cover"]["type"] == "upload"
|
||
assert result["cover"]["image_url"] == "https://example.com/cover.jpg"
|
||
# frame_time 保留默认值 None
|
||
assert result["cover"]["frame_time"] is None
|
||
|
||
def test_partial_title_override(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config({"title": {"text": "我的标题", "ai_auto": False}})
|
||
assert result["title"]["text"] == "我的标题"
|
||
assert result["title"]["ai_auto"] is False
|
||
# 其他字段保留默认值
|
||
assert result["title"]["font"] == "思源黑体"
|
||
assert result["title"]["size"] == 48
|
||
|
||
def test_non_standard_fields_preserved(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config({"generation_task_id": "task-123", "custom_key": "value"})
|
||
assert result["generation_task_id"] == "task-123"
|
||
assert result["custom_key"] == "value"
|
||
# 标准字段仍然完整
|
||
assert "cover" in result
|
||
assert "bgm" in result
|
||
|
||
def test_bgm_partial_override(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config({"bgm": {"volume": 0.8}})
|
||
assert result["bgm"]["volume"] == 0.8
|
||
assert result["bgm"]["source"] == "library"
|
||
assert result["bgm"]["asset_id"] == ""
|
||
|
||
def test_multiple_sections_override(self):
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
result = normalize_plan_config(
|
||
{
|
||
"cover": {"type": "manual", "frame_time": 5.0},
|
||
"title": {"enabled": False},
|
||
"subtitle": {"size": 36},
|
||
"bgm": {"source": "ai_recommend"},
|
||
}
|
||
)
|
||
assert result["cover"]["type"] == "manual"
|
||
assert result["cover"]["frame_time"] == 5.0
|
||
assert result["title"]["enabled"] is False
|
||
assert result["subtitle"]["size"] == 36
|
||
assert result["bgm"]["source"] == "ai_recommend"
|
||
|
||
|
||
class TestNormalizeTemplateConfig:
|
||
def test_same_as_plan_config_plus_template_fields(self):
|
||
"""template config 包含 plan config 的所有字段,外加 transition_enabled"""
|
||
from packages.domain.config_schemas import normalize_plan_config, normalize_template_config
|
||
|
||
raw = {"title": {"text": "模板标题"}}
|
||
plan_cfg = normalize_plan_config(raw)
|
||
tpl_cfg = normalize_template_config(raw)
|
||
# plan config 的字段在 template config 中应一致
|
||
for key in plan_cfg:
|
||
assert tpl_cfg[key] == plan_cfg[key]
|
||
# template config 额外包含 transition_enabled
|
||
assert "transition_enabled" in tpl_cfg
|
||
assert tpl_cfg["transition_enabled"] is True
|
||
|
||
def test_none_returns_defaults(self):
|
||
from packages.domain.config_schemas import DEFAULT_EDIT_TEMPLATE_CONFIG, normalize_template_config
|
||
|
||
result = normalize_template_config(None)
|
||
assert result == DEFAULT_EDIT_TEMPLATE_CONFIG.copy()
|
||
|
||
|
||
class TestConfigSchemas:
|
||
def test_cover_type_enum_values(self):
|
||
from packages.domain.config_schemas import CoverType
|
||
|
||
assert CoverType.AI_FRAME.value == "ai_frame"
|
||
assert CoverType.MANUAL.value == "manual"
|
||
assert CoverType.UPLOAD.value == "upload"
|
||
assert CoverType.AI_REGENERATE.value == "ai_regenerate"
|
||
|
||
def test_text_position_enum_values(self):
|
||
from packages.domain.config_schemas import TextPosition
|
||
|
||
assert TextPosition.TOP.value == "top"
|
||
assert TextPosition.CENTER.value == "center"
|
||
assert TextPosition.BOTTOM.value == "bottom"
|
||
|
||
def test_bgm_source_enum_values(self):
|
||
from packages.domain.config_schemas import BGMSource
|
||
|
||
assert BGMSource.LIBRARY.value == "library"
|
||
assert BGMSource.UPLOAD.value == "upload"
|
||
assert BGMSource.AI_RECOMMEND.value == "ai_recommend"
|
||
|
||
def test_cover_config_model(self):
|
||
from packages.domain.config_schemas import CoverConfig, CoverType
|
||
|
||
cfg = CoverConfig(type=CoverType.MANUAL, frame_time=3.5)
|
||
assert cfg.type == CoverType.MANUAL
|
||
assert cfg.frame_time == 3.5
|
||
assert cfg.image_url == ""
|
||
|
||
def test_title_config_defaults(self):
|
||
from packages.domain.config_schemas import TitleConfig
|
||
|
||
cfg = TitleConfig()
|
||
assert cfg.enabled is True
|
||
assert cfg.ai_auto is True
|
||
assert cfg.font == "思源黑体"
|
||
assert cfg.size == 48
|
||
assert cfg.bold is True
|
||
assert cfg.stroke.enabled is False
|
||
|
||
def test_bgm_config_validation(self):
|
||
from packages.domain.config_schemas import BGMConfig
|
||
|
||
cfg = BGMConfig(volume=0.5)
|
||
assert cfg.volume == 0.5
|
||
|
||
from pydantic import ValidationError
|
||
|
||
with pytest.raises(ValidationError):
|
||
BGMConfig(volume=1.5) # > 1.0 应该校验失败
|
||
|
||
def test_edit_plan_config_schema_full(self):
|
||
from packages.domain.config_schemas import EditPlanConfigSchema
|
||
|
||
schema = EditPlanConfigSchema()
|
||
assert schema.cover.type.value == "ai_frame"
|
||
assert schema.title.enabled is True
|
||
assert schema.subtitle.position.value == "bottom"
|
||
assert schema.bgm.source.value == "library"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# ai_tasks 单元测试
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestAIRunTasks:
|
||
def test_run_ai_recommend_returns_expected_structure(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend
|
||
|
||
result = run_ai_recommend(
|
||
plan_id="plan-001",
|
||
template_id="tpl-001",
|
||
asset_ids=["asset-1", "asset-2"],
|
||
editing_mode="one_take",
|
||
target_duration=30.0,
|
||
)
|
||
assert "clips" in result
|
||
assert "config" in result
|
||
assert "total_duration" in result
|
||
assert "confidence" in result
|
||
assert len(result["clips"]) >= 2 # 至少 intro + outro
|
||
assert result["total_duration"] > 0
|
||
assert 0 <= result["confidence"] <= 1
|
||
|
||
def test_run_ai_recommend_clip_structure(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend
|
||
|
||
result = run_ai_recommend(
|
||
plan_id="plan-001",
|
||
template_id="tpl-001",
|
||
asset_ids=["asset-1"],
|
||
)
|
||
for clip in result["clips"]:
|
||
assert "clip_type" in clip
|
||
assert "order" in clip
|
||
assert "duration" in clip
|
||
assert clip["duration"] > 0
|
||
|
||
def test_run_ai_recommend_empty_assets(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend
|
||
|
||
result = run_ai_recommend(
|
||
plan_id="plan-001",
|
||
template_id="tpl-001",
|
||
asset_ids=[],
|
||
)
|
||
# 即使没有素材,也应该有 intro + outro
|
||
assert len(result["clips"]) >= 2
|
||
|
||
def test_run_generate_cover_ai_frame(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover
|
||
|
||
result = run_generate_cover(
|
||
plan_id="plan-001",
|
||
asset_ids=["asset-1"],
|
||
cover_type="ai_frame",
|
||
)
|
||
assert result["type"] == "ai_frame"
|
||
assert "image_url" in result
|
||
assert "frame_time" in result
|
||
assert "confidence" in result
|
||
|
||
def test_run_generate_cover_manual(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover
|
||
|
||
result = run_generate_cover(
|
||
plan_id="plan-001",
|
||
asset_ids=["asset-1"],
|
||
cover_type="manual",
|
||
frame_time=5.0,
|
||
)
|
||
assert result["type"] == "manual"
|
||
assert result["frame_time"] == 5.0
|
||
|
||
def test_run_generate_cover_upload(self):
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover
|
||
|
||
result = run_generate_cover(
|
||
plan_id="plan-001",
|
||
asset_ids=[],
|
||
cover_type="upload",
|
||
)
|
||
assert result["type"] == "upload"
|