test(p3-1): 第八波 - config_schemas 领域单测 105个 #710

Merged
auto-approve-bot merged 2 commits from test/p3-1-wave8-config-schemas-tests into develop 2026-07-22 15:20:53 +08:00
+815
View File
@@ -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