diff --git a/tests/unit/test_config_schemas_domain.py b/tests/unit/test_config_schemas_domain.py new file mode 100755 index 000000000..3140d810b --- /dev/null +++ b/tests/unit/test_config_schemas_domain.py @@ -0,0 +1,815 @@ +""" +config_schemas 配置结构定义单元测试 + +覆盖: +- 4个枚举类型 +- 10个Pydantic模型 +- 2个normalize工具函数 +- 2个默认值常量 +""" + +import copy + +import pytest +from pydantic import ValidationError + +from packages.domain.config_schemas import ( + DEFAULT_EDIT_PLAN_CONFIG, + DEFAULT_EDIT_TEMPLATE_CONFIG, + BGMConfig, + BGMSource, + CoverConfig, + CoverType, + EditPlanConfigSchema, + EditTemplateConfigSchema, + ExportConfig, + FilterConfig, + ShadowConfig, + StrokeConfig, + SubtitleConfig, + TextAnimation, + TextPosition, + TitleConfig, + normalize_plan_config, + normalize_template_config, +) + +# ── 枚举测试 ────────────────────────────────────────────────────────────────── + + +class TestCoverType: + """CoverType 枚举测试""" + + def test_all_types_exist(self): + assert CoverType.AI_FRAME == "ai_frame" + assert CoverType.MANUAL == "manual" + assert CoverType.UPLOAD == "upload" + assert CoverType.AI_REGENERATE == "ai_regenerate" + + def test_total_count(self): + assert len(CoverType) == 4 + + def test_is_string_enum(self): + for t in CoverType: + assert isinstance(t.value, str) + assert isinstance(t, str) + + def test_string_comparison(self): + assert CoverType.AI_FRAME == "ai_frame" + assert CoverType.UPLOAD != "manual" + + +class TestTextPosition: + """TextPosition 枚举测试""" + + def test_all_positions_exist(self): + assert TextPosition.TOP == "top" + assert TextPosition.CENTER == "center" + assert TextPosition.BOTTOM == "bottom" + + def test_total_count(self): + assert len(TextPosition) == 3 + + def test_is_string_enum(self): + for p in TextPosition: + assert isinstance(p.value, str) + assert isinstance(p, str) + + +class TestTextAnimation: + """TextAnimation 枚举测试""" + + def test_all_animations_exist(self): + assert TextAnimation.NONE == "none" + assert TextAnimation.FADE_IN == "fade_in" + assert TextAnimation.SLIDE_UP == "slide_up" + assert TextAnimation.SLIDE_DOWN == "slide_down" + assert TextAnimation.SCALE == "scale" + + def test_total_count(self): + assert len(TextAnimation) == 5 + + def test_is_string_enum(self): + for a in TextAnimation: + assert isinstance(a.value, str) + assert isinstance(a, str) + + +class TestBGMSource: + """BGMSource 枚举测试""" + + def test_all_sources_exist(self): + assert BGMSource.LIBRARY == "library" + assert BGMSource.UPLOAD == "upload" + assert BGMSource.AI_RECOMMEND == "ai_recommend" + + def test_total_count(self): + assert len(BGMSource) == 3 + + def test_is_string_enum(self): + for s in BGMSource: + assert isinstance(s.value, str) + assert isinstance(s, str) + + +# ── 子结构模型测试 ──────────────────────────────────────────────────────────── + + +class TestStrokeConfig: + """StrokeConfig 描边配置测试""" + + def test_default_values(self): + config = StrokeConfig() + assert config.enabled is False + assert config.color == "#000000" + assert config.width == 1 + + def test_custom_values(self): + config = StrokeConfig(enabled=True, color="#ff0000", width=5) + assert config.enabled is True + assert config.color == "#ff0000" + assert config.width == 5 + + def test_width_min_boundary(self): + config = StrokeConfig(width=1) + assert config.width == 1 + + def test_width_max_boundary(self): + config = StrokeConfig(width=10) + assert config.width == 10 + + def test_width_below_min_raises(self): + with pytest.raises(ValidationError): + StrokeConfig(width=0) + + def test_width_above_max_raises(self): + with pytest.raises(ValidationError): + StrokeConfig(width=11) + + +class TestShadowConfig: + """ShadowConfig 阴影配置测试""" + + def test_default_values(self): + config = ShadowConfig() + assert config.enabled is False + assert config.blur == 4 + assert config.offset_x == 2 + assert config.offset_y == 2 + + def test_custom_values(self): + config = ShadowConfig(enabled=True, blur=10, offset_x=5, offset_y=3) + assert config.enabled is True + assert config.blur == 10 + assert config.offset_x == 5 + assert config.offset_y == 3 + + def test_blur_min_boundary(self): + config = ShadowConfig(blur=0) + assert config.blur == 0 + + def test_blur_max_boundary(self): + config = ShadowConfig(blur=20) + assert config.blur == 20 + + def test_blur_above_max_raises(self): + with pytest.raises(ValidationError): + ShadowConfig(blur=21) + + def test_negative_offset(self): + config = ShadowConfig(offset_x=-3, offset_y=-2) + assert config.offset_x == -3 + assert config.offset_y == -2 + + +class TestCoverConfig: + """CoverConfig 封面配置测试""" + + def test_default_values(self): + config = CoverConfig() + assert config.type == CoverType.AI_FRAME + assert config.image_url == "" + assert config.frame_time is None + + def test_custom_values(self): + config = CoverConfig(type=CoverType.MANUAL, image_url="http://img/1.jpg", frame_time=5.5) + assert config.type == CoverType.MANUAL + assert config.image_url == "http://img/1.jpg" + assert config.frame_time == 5.5 + + def test_frame_time_min_boundary(self): + config = CoverConfig(frame_time=0.0) + assert config.frame_time == 0.0 + + def test_frame_time_negative_raises(self): + with pytest.raises(ValidationError): + CoverConfig(frame_time=-1.0) + + def test_string_type_conversion(self): + """字符串枚举值自动转换""" + config = CoverConfig(type="upload") + assert config.type == CoverType.UPLOAD + + +class TestTitleConfig: + """TitleConfig 标题配置测试""" + + def test_default_values(self): + config = TitleConfig() + assert config.enabled is True + assert config.ai_auto is True + assert config.text == "" + assert config.position == TextPosition.TOP + assert config.font == "思源黑体" + assert config.color == "#ffffff" + assert config.size == 48 + assert config.bold is True + assert config.italic is False + assert isinstance(config.stroke, StrokeConfig) + assert isinstance(config.shadow, ShadowConfig) + + def test_custom_values(self): + config = TitleConfig( + enabled=False, + ai_auto=False, + text="测试标题", + position=TextPosition.BOTTOM, + font="微软雅黑", + color="#000000", + size=72, + bold=False, + italic=True, + ) + assert config.enabled is False + assert config.ai_auto is False + assert config.text == "测试标题" + assert config.position == TextPosition.BOTTOM + assert config.size == 72 + + def test_size_min_boundary(self): + config = TitleConfig(size=12) + assert config.size == 12 + + def test_size_max_boundary(self): + config = TitleConfig(size=120) + assert config.size == 120 + + def test_size_below_min_raises(self): + with pytest.raises(ValidationError): + TitleConfig(size=11) + + def test_size_above_max_raises(self): + with pytest.raises(ValidationError): + TitleConfig(size=121) + + def test_nested_stroke_config(self): + config = TitleConfig(stroke={"enabled": True, "width": 3}) + assert config.stroke.enabled is True + assert config.stroke.width == 3 + + def test_nested_shadow_config(self): + config = TitleConfig(shadow={"enabled": True, "blur": 8}) + assert config.shadow.enabled is True + assert config.shadow.blur == 8 + + +class TestSubtitleConfig: + """SubtitleConfig 字幕配置测试""" + + def test_default_values(self): + config = SubtitleConfig() + assert config.enabled is True + assert config.position == TextPosition.BOTTOM + assert config.font == "思源黑体" + assert config.color == "#ffffff" + assert config.size == 24 + assert config.animation == TextAnimation.FADE_IN + assert config.auto_generated is False + assert config.language == "" + assert config.max_chars_per_line == 20 + assert config.min_chars_per_segment == 8 + + def test_custom_values(self): + config = SubtitleConfig( + enabled=False, + position=TextPosition.TOP, + size=36, + animation=TextAnimation.SLIDE_UP, + auto_generated=True, + language="zh", + max_chars_per_line=30, + min_chars_per_segment=10, + ) + assert config.enabled is False + assert config.position == TextPosition.TOP + assert config.animation == TextAnimation.SLIDE_UP + assert config.auto_generated is True + assert config.language == "zh" + + def test_size_min_boundary(self): + config = SubtitleConfig(size=12) + assert config.size == 12 + + def test_size_max_boundary(self): + config = SubtitleConfig(size=60) + assert config.size == 60 + + def test_size_below_min_raises(self): + with pytest.raises(ValidationError): + SubtitleConfig(size=11) + + def test_max_chars_min_boundary(self): + config = SubtitleConfig(max_chars_per_line=8) + assert config.max_chars_per_line == 8 + + def test_max_chars_max_boundary(self): + config = SubtitleConfig(max_chars_per_line=40) + assert config.max_chars_per_line == 40 + + def test_min_chars_min_boundary(self): + config = SubtitleConfig(min_chars_per_segment=2) + assert config.min_chars_per_segment == 2 + + def test_min_chars_max_boundary(self): + config = SubtitleConfig(min_chars_per_segment=20) + assert config.min_chars_per_segment == 20 + + +class TestBGMConfig: + """BGMConfig 背景音乐配置测试""" + + def test_default_values(self): + config = BGMConfig() + assert config.enabled is False + assert config.source == BGMSource.LIBRARY + assert config.asset_id == "" + assert config.preset_id == "" + assert config.audio_url == "" + assert config.volume == 0.3 + assert config.fade_in == 0.0 + assert config.fade_out == 0.0 + assert config.loop_enabled is True + assert config.sidechain_enabled is False + assert config.sidechain_ratio == 0.3 + assert config.sidechain_attack == 0.02 + assert config.sidechain_release == 0.5 + assert config.sidechain_threshold == -25.0 + + def test_custom_values(self): + config = BGMConfig( + enabled=True, + source=BGMSource.UPLOAD, + asset_id="bgm_001", + volume=0.5, + fade_in=2.0, + fade_out=3.0, + loop_enabled=False, + sidechain_enabled=True, + sidechain_ratio=0.5, + sidechain_attack=0.05, + sidechain_release=1.0, + sidechain_threshold=-20.0, + ) + assert config.enabled is True + assert config.source == BGMSource.UPLOAD + assert config.asset_id == "bgm_001" + assert config.volume == 0.5 + assert config.loop_enabled is False + assert config.sidechain_enabled is True + assert config.sidechain_ratio == 0.5 + + def test_volume_min_boundary(self): + config = BGMConfig(volume=0.0) + assert config.volume == 0.0 + + def test_volume_max_boundary(self): + config = BGMConfig(volume=1.0) + assert config.volume == 1.0 + + def test_volume_above_max_raises(self): + with pytest.raises(ValidationError): + BGMConfig(volume=1.1) + + def test_fade_in_max_boundary(self): + config = BGMConfig(fade_in=30.0) + assert config.fade_in == 30.0 + + def test_fade_in_above_max_raises(self): + with pytest.raises(ValidationError): + BGMConfig(fade_in=31.0) + + def test_sidechain_ratio_range(self): + config = BGMConfig(sidechain_ratio=0.0) + assert config.sidechain_ratio == 0.0 + config = BGMConfig(sidechain_ratio=1.0) + assert config.sidechain_ratio == 1.0 + + def test_sidechain_attack_min(self): + config = BGMConfig(sidechain_attack=0.001) + assert config.sidechain_attack == 0.001 + + def test_sidechain_attack_below_min_raises(self): + with pytest.raises(ValidationError): + BGMConfig(sidechain_attack=0.0001) + + def test_sidechain_threshold_range(self): + config = BGMConfig(sidechain_threshold=-60.0) + assert config.sidechain_threshold == -60.0 + config = BGMConfig(sidechain_threshold=0.0) + assert config.sidechain_threshold == 0.0 + + def test_sidechain_threshold_out_of_range_raises(self): + with pytest.raises(ValidationError): + BGMConfig(sidechain_threshold=-61.0) + with pytest.raises(ValidationError): + BGMConfig(sidechain_threshold=1.0) + + +class TestExportConfig: + """ExportConfig 导出配置测试""" + + def test_default_values(self): + config = ExportConfig() + assert config.resolution == "1080x1920" + assert config.fps == 30 + assert config.video_bitrate == 8000 + assert config.audio_bitrate == 128 + assert config.format == "mp4" + assert config.quality_preset == "balanced" + assert config.watermark_enabled is False + assert config.watermark_text == "" + + def test_custom_values(self): + config = ExportConfig( + resolution="2160x3840", + fps=60, + video_bitrate=20000, + audio_bitrate=320, + format="mov", + quality_preset="best", + watermark_enabled=True, + watermark_text="测试水印", + ) + assert config.resolution == "2160x3840" + assert config.fps == 60 + assert config.video_bitrate == 20000 + assert config.format == "mov" + + def test_fps_min_boundary(self): + config = ExportConfig(fps=15) + assert config.fps == 15 + + def test_fps_max_boundary(self): + config = ExportConfig(fps=60) + assert config.fps == 60 + + def test_fps_below_min_raises(self): + with pytest.raises(ValidationError): + ExportConfig(fps=14) + + def test_fps_above_max_raises(self): + with pytest.raises(ValidationError): + ExportConfig(fps=61) + + def test_video_bitrate_range(self): + config = ExportConfig(video_bitrate=1000) + assert config.video_bitrate == 1000 + config = ExportConfig(video_bitrate=20000) + assert config.video_bitrate == 20000 + + def test_audio_bitrate_range(self): + config = ExportConfig(audio_bitrate=64) + assert config.audio_bitrate == 64 + config = ExportConfig(audio_bitrate=320) + assert config.audio_bitrate == 320 + + +class TestFilterConfig: + """FilterConfig 滤镜配置测试""" + + def test_default_values(self): + config = FilterConfig() + assert config.enabled is False + assert config.preset_id == "filter_none" + assert config.intensity == 100 + assert config.brightness == 0.0 + assert config.contrast == 1.0 + assert config.saturation == 1.0 + assert config.warmth == 0.0 + + def test_custom_values(self): + config = FilterConfig( + enabled=True, + preset_id="filter_vintage", + intensity=50, + brightness=0.5, + contrast=1.5, + saturation=2.0, + warmth=0.3, + ) + assert config.enabled is True + assert config.preset_id == "filter_vintage" + assert config.intensity == 50 + assert config.brightness == 0.5 + + def test_intensity_min_boundary(self): + config = FilterConfig(intensity=0) + assert config.intensity == 0 + + def test_intensity_max_boundary(self): + config = FilterConfig(intensity=100) + assert config.intensity == 100 + + def test_intensity_out_of_range_raises(self): + with pytest.raises(ValidationError): + FilterConfig(intensity=-1) + with pytest.raises(ValidationError): + FilterConfig(intensity=101) + + def test_brightness_range(self): + config = FilterConfig(brightness=-1.0) + assert config.brightness == -1.0 + config = FilterConfig(brightness=1.0) + assert config.brightness == 1.0 + + def test_contrast_range(self): + config = FilterConfig(contrast=0.0) + assert config.contrast == 0.0 + config = FilterConfig(contrast=2.0) + assert config.contrast == 2.0 + + def test_saturation_range(self): + config = FilterConfig(saturation=0.0) + assert config.saturation == 0.0 + config = FilterConfig(saturation=3.0) + assert config.saturation == 3.0 + + def test_warmth_range(self): + config = FilterConfig(warmth=-1.0) + assert config.warmth == -1.0 + config = FilterConfig(warmth=1.0) + assert config.warmth == 1.0 + + def test_brightness_out_of_range_raises(self): + with pytest.raises(ValidationError): + FilterConfig(brightness=-1.1) + with pytest.raises(ValidationError): + FilterConfig(brightness=1.1) + + +# ── 完整 config 模型测试 ───────────────────────────────────────────────────── + + +class TestEditPlanConfigSchema: + """EditPlanConfigSchema 完整计划配置测试""" + + def test_default_values(self): + config = EditPlanConfigSchema() + assert isinstance(config.cover, CoverConfig) + assert isinstance(config.title, TitleConfig) + assert isinstance(config.subtitle, SubtitleConfig) + assert isinstance(config.bgm, BGMConfig) + assert isinstance(config.export, ExportConfig) + assert isinstance(config.filter, FilterConfig) + assert config.editing_mode == "one_take" + + def test_partial_update(self): + """只传部分字段,其余保持默认""" + config = EditPlanConfigSchema( + title={"enabled": False}, + bgm={"enabled": True, "volume": 0.5}, + editing_mode="pip", + ) + assert config.title.enabled is False + assert config.bgm.enabled is True + assert config.bgm.volume == 0.5 + assert config.editing_mode == "pip" + # 其他字段保持默认 + assert config.cover.type == CoverType.AI_FRAME + assert config.subtitle.enabled is True + + def test_nested_model_preservation(self): + """嵌套模型完整可用""" + config = EditPlanConfigSchema() + assert config.title.stroke.enabled is False + assert config.title.shadow.blur == 4 + assert config.bgm.sidechain_ratio == 0.3 + + +class TestEditTemplateConfigSchema: + """EditTemplateConfigSchema 模板配置测试""" + + def test_default_values(self): + config = EditTemplateConfigSchema() + assert isinstance(config.cover, CoverConfig) + assert isinstance(config.title, TitleConfig) + assert isinstance(config.subtitle, SubtitleConfig) + assert isinstance(config.bgm, BGMConfig) + assert isinstance(config.export, ExportConfig) + assert isinstance(config.filter, FilterConfig) + assert config.editing_mode == "one_take" + assert config.transition_enabled is True + + def test_transition_enabled_false(self): + config = EditTemplateConfigSchema(transition_enabled=False) + assert config.transition_enabled is False + + def test_partial_update(self): + config = EditTemplateConfigSchema( + filter={"enabled": True, "preset_id": "filter_cinematic"}, + transition_enabled=False, + editing_mode="voice_over", + ) + assert config.filter.enabled is True + assert config.filter.preset_id == "filter_cinematic" + assert config.transition_enabled is False + assert config.editing_mode == "voice_over" + + +# ── 默认值常量测试 ──────────────────────────────────────────────────────────── + + +class TestDefaultConfigs: + """默认值常量测试""" + + def test_plan_config_structure(self): + """DEFAULT_EDIT_PLAN_CONFIG 结构完整""" + assert "cover" in DEFAULT_EDIT_PLAN_CONFIG + assert "title" in DEFAULT_EDIT_PLAN_CONFIG + assert "subtitle" in DEFAULT_EDIT_PLAN_CONFIG + assert "bgm" in DEFAULT_EDIT_PLAN_CONFIG + assert "export" in DEFAULT_EDIT_PLAN_CONFIG + assert "filter" in DEFAULT_EDIT_PLAN_CONFIG + assert "editing_mode" in DEFAULT_EDIT_PLAN_CONFIG + + def test_template_config_has_transition(self): + """模板配置比计划配置多一个 transition_enabled""" + assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG + assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True + + def test_template_extends_plan(self): + """模板配置是计划配置的超集""" + for key in DEFAULT_EDIT_PLAN_CONFIG: + assert key in DEFAULT_EDIT_TEMPLATE_CONFIG + + def test_defaults_are_deep_copy_safe(self): + """修改默认值不会影响常量本身""" + original = copy.deepcopy(DEFAULT_EDIT_PLAN_CONFIG) + config = EditPlanConfigSchema() + config.title.text = "modified" + assert DEFAULT_EDIT_PLAN_CONFIG == original + + +# ── normalize 函数测试 ──────────────────────────────────────────────────────── + + +class TestNormalizePlanConfig: + """normalize_plan_config 函数测试""" + + def test_none_returns_default(self): + result = normalize_plan_config(None) + assert result == DEFAULT_EDIT_PLAN_CONFIG + # 确保是深拷贝 + result["cover"]["type"] = "manual" + assert DEFAULT_EDIT_PLAN_CONFIG["cover"]["type"] == "ai_frame" + + def test_empty_dict_returns_default(self): + result = normalize_plan_config({}) + assert result == DEFAULT_EDIT_PLAN_CONFIG + + def test_partial_cover_update(self): + raw = {"cover": {"type": "manual", "frame_time": 5.0}} + result = normalize_plan_config(raw) + assert result["cover"]["type"] == "manual" + assert result["cover"]["frame_time"] == 5.0 + # 其他字段保留默认 + assert result["cover"]["image_url"] == "" + + def test_partial_title_update(self): + raw = {"title": {"enabled": False, "text": "自定义标题"}} + result = normalize_plan_config(raw) + assert result["title"]["enabled"] is False + assert result["title"]["text"] == "自定义标题" + assert result["title"]["size"] == 48 # 默认值保留 + + def test_partial_subtitle_update(self): + raw = {"subtitle": {"enabled": False, "size": 32}} + result = normalize_plan_config(raw) + assert result["subtitle"]["enabled"] is False + assert result["subtitle"]["size"] == 32 + + def test_partial_bgm_update(self): + raw = {"bgm": {"enabled": True, "volume": 0.8}} + result = normalize_plan_config(raw) + assert result["bgm"]["enabled"] is True + assert result["bgm"]["volume"] == 0.8 + + def test_editing_mode_update(self): + raw = {"editing_mode": "pip"} + result = normalize_plan_config(raw) + assert result["editing_mode"] == "pip" + + def test_non_standard_fields_preserved(self): + """非标准字段会被保留(透传)""" + raw = {"generation_task_id": "task_123", "custom_field": "value"} + result = normalize_plan_config(raw) + assert result["generation_task_id"] == "task_123" + assert result["custom_field"] == "value" + + def test_full_update(self): + """多个字段同时更新""" + raw = { + "cover": {"type": "upload", "image_url": "http://img.jpg"}, + "title": {"enabled": False}, + "bgm": {"enabled": True, "volume": 0.5}, + "editing_mode": "voice_pip", + "extra_key": "extra_val", + } + result = normalize_plan_config(raw) + assert result["cover"]["type"] == "upload" + assert result["title"]["enabled"] is False + assert result["bgm"]["enabled"] is True + assert result["bgm"]["volume"] == 0.5 + assert result["editing_mode"] == "voice_pip" + assert result["extra_key"] == "extra_val" + + def test_non_dict_section_ignored(self): + """section不是dict时忽略""" + raw = {"cover": "not_a_dict"} + result = normalize_plan_config(raw) + # cover 应保持默认值 + assert result["cover"]["type"] == "ai_frame" + # 但 cover 字段本身作为非标准字段保留 + # 不对,看实现:只有当 isinstance(raw[section_key], dict) 时才 update + # 非dict的section会作为非标准字段保留吗?看实现: + # section_key in raw and isinstance -> update + # 然后遍历所有key,不在标准列表中的才会保留 + # cover 在标准列表中,所以不会被保留为非标准字段 + # 所以结果应该是默认的cover配置 + assert isinstance(result["cover"], dict) + assert result["cover"]["type"] == "ai_frame" + + def test_editing_mode_non_string_ignored(self): + """editing_mode不是字符串时忽略""" + raw = {"editing_mode": 123} + result = normalize_plan_config(raw) + # 作为非标准字段保留?不,看实现: + # 如果 "editing_mode" in raw and isinstance(str) 才更新 + # 然后遍历所有key,不在标准列表中的保留 + # editing_mode 在标准列表中,所以不会保留为非标准 + # 所以 editing_mode 保持默认 + assert result["editing_mode"] == "one_take" + + +class TestNormalizeTemplateConfig: + """normalize_template_config 函数测试""" + + def test_none_returns_default(self): + result = normalize_template_config(None) + assert result == DEFAULT_EDIT_TEMPLATE_CONFIG + # 确保是深拷贝 + result["transition_enabled"] = False + assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True + + def test_empty_dict_returns_default(self): + result = normalize_template_config({}) + assert result == DEFAULT_EDIT_TEMPLATE_CONFIG + + def test_transition_enabled_update(self): + raw = {"transition_enabled": False} + result = normalize_template_config(raw) + assert result["transition_enabled"] is False + + def test_transition_enabled_non_bool_ignored(self): + raw = {"transition_enabled": "yes"} + result = normalize_template_config(raw) + # transition_enabled 在标准列表中,非bool则不更新 + # 也不会作为非标准字段保留 + assert result["transition_enabled"] is True + + def test_partial_sections_update(self): + raw = { + "cover": {"type": "ai_regenerate"}, + "filter": {"enabled": True, "intensity": 75}, + "transition_enabled": False, + "editing_mode": "pip", + } + result = normalize_template_config(raw) + assert result["cover"]["type"] == "ai_regenerate" + assert result["filter"]["enabled"] is True + assert result["filter"]["intensity"] == 75 + assert result["transition_enabled"] is False + assert result["editing_mode"] == "pip" + + def test_non_standard_fields_preserved(self): + raw = {"template_version": 3, "author": "test"} + result = normalize_template_config(raw) + assert result["template_version"] == 3 + assert result["author"] == "test" + + def test_template_has_extra_field_over_plan(self): + """模板normalize结果比计划多transition_enabled""" + plan_result = normalize_plan_config({"bgm": {"enabled": True}}) + template_result = normalize_template_config({"bgm": {"enabled": True}}) + assert "transition_enabled" in template_result + assert "transition_enabled" not in plan_result