Files
xiaoxia-saas/tests/unit/test_config_schemas_and_ai_endpoints.py
xiaoxia 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
feat(phase3): 下线/edit-plans/*旧路由 + 清理废弃代码 + 数据库冗余字段清理 (#657)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-20 23:48:36 +08:00

268 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"