"""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"