Files
xiaoxia-saas/tests/unit/test_template_clip_converter.py
xiaoxia 88ca8b4406
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
test(wave145): template_clip_converter 单测补全 +79 (#1068)
2026-07-28 12:23:52 +08:00

779 lines
28 KiB
Python
Executable File

"""template_clip_converter 模板片段转换器单元测试."""
from __future__ import annotations
from dataclasses import dataclass
from types import SimpleNamespace
import pytest
from packages.domain.template_clip_config import (
ClipType,
TemplateClipConfig,
TransitionEffect,
)
from packages.domain.template_clip_converter import (
_enum_value,
clip_config_to_snapshot,
clip_configs_to_snapshots,
clip_to_template_clip_config,
clips_to_template_clip_configs,
filter_clip_config,
filter_plan_config_to_template,
safe_parse_clip_type,
safe_parse_transition_effect,
snapshot_to_template_clip_config,
snapshots_to_template_clip_configs,
validate_template_name,
)
# ── 安全枚举解析测试 ──────────────────────────────────────────────────────────
class TestSafeParseTransitionEffect:
"""safe_parse_transition_effect 测试."""
def test_enum_passthrough(self):
"""传入枚举直接返回."""
result = safe_parse_transition_effect(TransitionEffect.FADE)
assert result == TransitionEffect.FADE
assert isinstance(result, TransitionEffect)
def test_valid_string(self):
"""有效字符串解析."""
assert safe_parse_transition_effect("fade") == TransitionEffect.FADE
assert safe_parse_transition_effect("cut") == TransitionEffect.CUT
assert safe_parse_transition_effect("dissolve") == TransitionEffect.DISSOLVE
def test_invalid_string_returns_default(self):
"""无效字符串返回默认值."""
result = safe_parse_transition_effect("invalid_effect")
assert result == TransitionEffect.CUT # 默认CUT
def test_none_returns_default(self):
"""None返回默认值."""
result = safe_parse_transition_effect(None)
assert result == TransitionEffect.CUT
def test_custom_default(self):
"""自定义默认值."""
result = safe_parse_transition_effect("bad", default=TransitionEffect.FADE)
assert result == TransitionEffect.FADE
def test_int_returns_default(self):
"""数字类型返回默认."""
result = safe_parse_transition_effect(123)
assert result == TransitionEffect.CUT
def test_all_valid_strings(self):
"""所有有效字符串都能解析."""
for te in TransitionEffect:
result = safe_parse_transition_effect(te.value)
assert result == te
class TestSafeParseClipType:
"""safe_parse_clip_type 测试."""
def test_enum_passthrough(self):
"""传入枚举直接返回."""
result = safe_parse_clip_type(ClipType.INTRO)
assert result == ClipType.INTRO
assert isinstance(result, ClipType)
def test_valid_string(self):
"""有效字符串解析."""
assert safe_parse_clip_type("intro") == ClipType.INTRO
assert safe_parse_clip_type("main") == ClipType.MAIN
assert safe_parse_clip_type("outro") == ClipType.OUTRO
assert safe_parse_clip_type("title") == ClipType.TITLE
def test_invalid_string_returns_default(self):
"""无效字符串返回默认值."""
result = safe_parse_clip_type("invalid_type")
assert result == ClipType.MAIN # 默认MAIN
def test_none_returns_default(self):
"""None返回默认值."""
result = safe_parse_clip_type(None)
assert result == ClipType.MAIN
def test_custom_default(self):
"""自定义默认值."""
result = safe_parse_clip_type("bad", default=ClipType.TRANSITION)
assert result == ClipType.TRANSITION
def test_all_valid_strings(self):
"""所有有效字符串都能解析."""
for ct in ClipType:
result = safe_parse_clip_type(ct.value)
assert result == ct
# ── _enum_value 内部函数测试 ─────────────────────────────────────────────────
class TestEnumValue:
"""_enum_value 内部工具函数测试."""
def test_enum_value(self):
"""枚举返回value."""
assert _enum_value(TransitionEffect.FADE) == "fade"
assert _enum_value(ClipType.MAIN) == "main"
def test_string_passthrough(self):
"""字符串直接返回."""
assert _enum_value("hello") == "hello"
def test_int_passthrough(self):
"""数字直接返回."""
assert _enum_value(42) == 42
def test_none_passthrough(self):
"""None直接返回."""
assert _enum_value(None) is None
# ── filter_clip_config 测试 ──────────────────────────────────────────────────
class TestFilterClipConfig:
"""filter_clip_config 测试."""
def test_none_config_no_speed(self):
"""None config + 无playback_speed."""
result = filter_clip_config(None)
assert result == {}
def test_none_config_with_speed(self):
"""None config + 有playback_speed."""
result = filter_clip_config(None, playback_speed=1.5)
assert result == {"playback_speed": 1.5}
def test_speed_equals_one_not_included(self):
"""playback_speed=1.0不写入."""
result = filter_clip_config({"key": "val"}, playback_speed=1.0)
assert "playback_speed" not in result
assert result["key"] == "val"
def test_speed_not_one_included(self):
"""playback_speed≠1.0写入."""
result = filter_clip_config({"key": "val"}, playback_speed=0.8)
assert result["playback_speed"] == 0.8
assert result["key"] == "val"
def test_speed_none_not_included(self):
"""playback_speed=None不写入."""
result = filter_clip_config({"key": "val"}, playback_speed=None)
assert "playback_speed" not in result
def test_removes_asset_info(self):
"""移除asset_info字段."""
config = {"a": 1, "asset_info": {"name": "x"}, "b": 2}
result = filter_clip_config(config)
assert "asset_info" not in result
assert result["a"] == 1
assert result["b"] == 2
def test_removes_source_asset_id(self):
"""移除source_asset_id字段."""
config = {"source_asset_id": "abc", "other": "val"}
result = filter_clip_config(config)
assert "source_asset_id" not in result
assert result["other"] == "val"
def test_custom_skip_keys(self):
"""自定义跳过字段."""
config = {"keep": "yes", "skip_me": "no", "also_skip": "no"}
skip = frozenset({"skip_me", "also_skip"})
result = filter_clip_config(config, skip_keys=skip)
assert "keep" in result
assert "skip_me" not in result
assert "also_skip" not in result
def test_empty_config(self):
"""空dict config."""
result = filter_clip_config({})
assert result == {}
def test_does_not_mutate_original(self):
"""不修改原始config."""
original = {"a": 1, "asset_info": "x"}
original_copy = dict(original)
filter_clip_config(original)
assert original == original_copy
def test_speed_overrides_config(self):
"""playback_speed 覆盖 clip_config 中的值."""
result = filter_clip_config({"playback_speed": 2.0}, playback_speed=0.5)
# clip_config是后面合并的,所以会覆盖前面的playback_speed
assert result["playback_speed"] == 2.0
# ── filter_plan_config_to_template 测试 ──────────────────────────────────────
class TestFilterPlanConfigToTemplate:
"""filter_plan_config_to_template 测试."""
def test_none_config(self):
"""None config返回空dict."""
result = filter_plan_config_to_template(None)
assert result == {}
def test_empty_config(self):
"""空dict返回空."""
result = filter_plan_config_to_template({})
assert result == {}
def test_removes_runtime_fields(self):
"""移除运行时字段."""
config = {
"keep": "yes",
"is_template_draft": True,
"asset_ids": ["a", "b"],
"source_edit_plan_id": "plan_123",
"generation_task_id": "task_456",
"another_field": "also_keep",
}
result = filter_plan_config_to_template(config)
assert "keep" in result
assert "another_field" in result
assert "is_template_draft" not in result
assert "asset_ids" not in result
assert "source_edit_plan_id" not in result
assert "generation_task_id" not in result
def test_custom_skip_keys(self):
"""自定义跳过字段."""
config = {"keep": "yes", "skip_a": "no", "skip_b": "no"}
skip = frozenset({"skip_a", "skip_b"})
result = filter_plan_config_to_template(config, skip_keys=skip)
assert "keep" in result
assert "skip_a" not in result
assert "skip_b" not in result
def test_preserves_values(self):
"""保留字段的值不变."""
config = {"title": "测试", "duration": 10.5, "items": [1, 2, 3]}
result = filter_plan_config_to_template(config)
assert result["title"] == "测试"
assert result["duration"] == 10.5
assert result["items"] == [1, 2, 3]
# ── clip_to_template_clip_config 测试 ────────────────────────────────────────
class TestClipToTemplateClipConfig:
"""clip_to_template_clip_config 测试."""
def _make_clip(self, **kwargs):
"""创建模拟clip对象."""
defaults = {
"clip_type": "main",
"order": 1,
"duration": 5.0,
"text_content": "默认文案",
"transition_effect": "cut",
"playback_speed": 1.0,
"config": {},
}
defaults.update(kwargs)
return SimpleNamespace(**defaults)
def test_basic_conversion(self):
"""基础转换."""
clip = self._make_clip(order=2, duration=3.5)
result = clip_to_template_clip_config("tpl_001", clip)
assert isinstance(result, TemplateClipConfig)
assert result.template_id == "tpl_001"
assert result.clip_type == ClipType.MAIN
assert result.order == 2
assert result.min_duration == 3.5
assert result.max_duration == 3.5
assert result.text_template == "默认文案"
def test_intro_clip_type(self):
"""intro类型转换."""
clip = self._make_clip(clip_type="intro", order=0)
result = clip_to_template_clip_config("t1", clip)
assert result.clip_type == ClipType.INTRO
def test_transition_effect(self):
"""转场效果转换."""
clip = self._make_clip(transition_effect="fade")
result = clip_to_template_clip_config("t1", clip)
assert result.transition_effect == TransitionEffect.FADE
def test_fixed_duration(self):
"""固定时长:min=max=duration."""
clip = self._make_clip(duration=7.2)
result = clip_to_template_clip_config("t1", clip)
assert result.min_duration == 7.2
assert result.max_duration == 7.2
assert result.min_duration == result.max_duration
def test_zero_duration(self):
"""duration为0."""
clip = self._make_clip(duration=0)
result = clip_to_template_clip_config("t1", clip)
assert result.min_duration == 0.0
assert result.max_duration == 0.0
def test_none_duration(self):
"""duration为None时默认0."""
clip = self._make_clip(duration=None)
result = clip_to_template_clip_config("t1", clip)
assert result.min_duration == 0.0
def test_text_content_mapped_to_text_template(self):
"""text_content → text_template."""
clip = self._make_clip(text_content="你好世界")
result = clip_to_template_clip_config("t1", clip)
assert result.text_template == "你好世界"
def test_none_text_content(self):
"""text_content为None时默认空字符串."""
clip = self._make_clip(text_content=None)
result = clip_to_template_clip_config("t1", clip)
assert result.text_template == ""
def test_playback_speed_in_config(self):
"""非1.0的playback_speed存入config."""
clip = self._make_clip(playback_speed=1.5)
result = clip_to_template_clip_config("t1", clip)
assert result.config["playback_speed"] == 1.5
def test_playback_speed_one_not_in_config(self):
"""playback_speed=1.0不存入config."""
clip = self._make_clip(playback_speed=1.0)
result = clip_to_template_clip_config("t1", clip)
assert "playback_speed" not in result.config
def test_config_passed_through(self):
"""clip.config传递到结果."""
clip = self._make_clip(config={"font_size": 24, "color": "red"})
result = clip_to_template_clip_config("t1", clip)
assert result.config["font_size"] == 24
assert result.config["color"] == "red"
def test_config_filters_asset_fields(self):
"""config中过滤素材字段."""
clip = self._make_clip(config={"asset_info": {"x": 1}, "keep": "yes"})
result = clip_to_template_clip_config("t1", clip)
assert "asset_info" not in result.config
assert result.config["keep"] == "yes"
def test_invalid_clip_type_defaults_to_main(self):
"""无效clip_type安全解析为main."""
clip = self._make_clip(clip_type="unknown_type")
result = clip_to_template_clip_config("t1", clip)
assert result.clip_type == ClipType.MAIN
def test_clip_without_attributes(self):
"""缺少属性的对象使用默认值."""
@dataclass
class MinimalClip:
order: int = 0
clip = MinimalClip(order=5)
result = clip_to_template_clip_config("t1", clip)
assert result.clip_type == ClipType.MAIN # 默认
assert result.order == 5
assert result.text_template == ""
assert result.transition_effect == TransitionEffect.CUT
class TestClipsToTemplateClipConfigs:
"""clips_to_template_clip_configs 批量转换测试."""
def test_empty_list(self):
"""空列表返回空列表."""
result = clips_to_template_clip_configs("t1", [])
assert result == []
def test_multiple_clips(self):
"""多个片段转换."""
clips = [
SimpleNamespace(
clip_type="intro",
order=0,
duration=2.0,
text_content="",
transition_effect="fade",
playback_speed=1.0,
config={},
),
SimpleNamespace(
clip_type="main",
order=1,
duration=5.0,
text_content="主体",
transition_effect="cut",
playback_speed=1.0,
config={},
),
SimpleNamespace(
clip_type="outro",
order=2,
duration=3.0,
text_content="结束",
transition_effect="fade",
playback_speed=1.0,
config={},
),
]
result = clips_to_template_clip_configs("tpl_multi", clips)
assert len(result) == 3
assert result[0].clip_type == ClipType.INTRO
assert result[0].order == 0
assert result[1].clip_type == ClipType.MAIN
assert result[1].order == 1
assert result[2].clip_type == ClipType.OUTRO
assert result[2].order == 2
assert all(r.template_id == "tpl_multi" for r in result)
# ── clip_config_to_snapshot 测试 ────────────────────────────────────────────
class TestClipConfigToSnapshot:
"""clip_config_to_snapshot 测试."""
def test_basic_snapshot(self):
"""基础快照转换."""
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type="main",
order=1,
min_duration=3.0,
max_duration=5.0,
text_template="hello",
transition_effect="fade",
config={"key": "val"},
)
snap = clip_config_to_snapshot(cfg)
assert snap["clip_type"] == "main"
assert snap["order"] == 1
assert snap["min_duration"] == 3.0
assert snap["max_duration"] == 5.0
assert snap["text_template"] == "hello"
assert snap["transition_effect"] == "fade"
assert snap["config"] == {"key": "val"}
def test_enum_values_in_snapshot(self):
"""枚举转为字符串value."""
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type=ClipType.TITLE,
order=0,
transition_effect=TransitionEffect.DISSOLVE,
)
snap = clip_config_to_snapshot(cfg)
assert snap["clip_type"] == "title"
assert isinstance(snap["clip_type"], str)
assert snap["transition_effect"] == "dissolve"
assert isinstance(snap["transition_effect"], str)
def test_empty_text_template(self):
"""空文案模板."""
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type="main",
order=0,
text_template="",
)
snap = clip_config_to_snapshot(cfg)
assert snap["text_template"] == ""
def test_none_text_template_becomes_empty(self):
"""text_template为None时返回空串."""
# TemplateClipConfig的text_template默认是空串,模拟一个有None属性的对象
obj = SimpleNamespace(
clip_type=ClipType.MAIN,
order=0,
min_duration=1.0,
max_duration=2.0,
text_template=None,
transition_effect=TransitionEffect.CUT,
config={},
)
snap = clip_config_to_snapshot(obj)
assert snap["text_template"] == ""
def test_config_is_copy(self):
"""config是副本,不是原对象引用."""
original_config = {"a": 1}
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type="main",
order=0,
config=original_config,
)
snap = clip_config_to_snapshot(cfg)
snap["config"]["b"] = 2
assert "b" not in cfg.config
assert original_config.get("b") is None
def test_none_config_becomes_empty(self):
"""config为None时返回空dict."""
obj = SimpleNamespace(
clip_type="main",
order=0,
min_duration=0,
max_duration=0,
text_template="",
transition_effect="cut",
config=None,
)
snap = clip_config_to_snapshot(obj)
assert snap["config"] == {}
def test_default_values_for_missing_attrs(self):
"""缺少属性时用默认值."""
obj = SimpleNamespace()
snap = clip_config_to_snapshot(obj)
assert snap["clip_type"] is None # _enum_value(None)=None
assert snap["order"] == 0
assert snap["min_duration"] == 0.0
assert snap["max_duration"] == 0.0
assert snap["text_template"] == ""
assert snap["transition_effect"] is None
assert snap["config"] == {}
def test_snapshot_keys(self):
"""快照包含所有预期字段."""
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type="main",
order=0,
)
snap = clip_config_to_snapshot(cfg)
expected_keys = {
"clip_type",
"order",
"min_duration",
"max_duration",
"text_template",
"transition_effect",
"config",
}
assert set(snap.keys()) == expected_keys
class TestClipConfigsToSnapshots:
"""clip_configs_to_snapshots 批量测试."""
def test_empty_list(self):
"""空列表返回空列表."""
assert clip_configs_to_snapshots([]) == []
def test_multiple_configs(self):
"""多个配置转换."""
configs = [
TemplateClipConfig.create("t1", "intro", 0, min_duration=2.0, max_duration=2.0),
TemplateClipConfig.create("t1", "main", 1, min_duration=3.0, max_duration=5.0),
TemplateClipConfig.create("t1", "outro", 2, min_duration=1.0, max_duration=1.0),
]
snaps = clip_configs_to_snapshots(configs)
assert len(snaps) == 3
assert snaps[0]["clip_type"] == "intro"
assert snaps[1]["clip_type"] == "main"
assert snaps[2]["clip_type"] == "outro"
# ── snapshot_to_template_clip_config 测试 ────────────────────────────────────
class TestSnapshotToTemplateClipConfig:
"""snapshot_to_template_clip_config 测试."""
def test_basic_conversion(self):
"""基础快照→配置转换."""
snap = {
"clip_type": "main",
"order": 2,
"min_duration": 3.0,
"max_duration": 5.0,
"text_template": "hello",
"transition_effect": "fade",
"config": {"key": "val"},
}
result = snapshot_to_template_clip_config("tpl_new", snap)
assert isinstance(result, TemplateClipConfig)
assert result.template_id == "tpl_new"
assert result.clip_type == ClipType.MAIN
assert result.order == 2
assert result.min_duration == 3.0
assert result.max_duration == 5.0
assert result.text_template == "hello"
assert result.transition_effect == TransitionEffect.FADE
assert result.config == {"key": "val"}
def test_defaults_for_missing_keys(self):
"""缺失字段使用默认值."""
snap: dict = {}
result = snapshot_to_template_clip_config("t1", snap)
assert result.clip_type == ClipType.MAIN # 默认main
assert result.order == 0
assert result.min_duration == 0.0
assert result.max_duration == 0.0
assert result.text_template == ""
assert result.transition_effect == TransitionEffect.CUT # 默认cut
assert result.config == {}
def test_intro_type(self):
"""intro类型解析."""
snap = {"clip_type": "intro", "order": 0}
result = snapshot_to_template_clip_config("t1", snap)
assert result.clip_type == ClipType.INTRO
def test_invalid_clip_type_defaults(self):
"""无效clip_type安全回退到main."""
snap = {"clip_type": "weird_type", "order": 0}
result = snapshot_to_template_clip_config("t1", snap)
assert result.clip_type == ClipType.MAIN
def test_invalid_transition_defaults(self):
"""无效transition_effect安全回退到cut."""
snap = {"transition_effect": "invalid_fx"}
result = snapshot_to_template_clip_config("t1", snap)
assert result.transition_effect == TransitionEffect.CUT
def test_none_config_becomes_empty(self):
"""config为None时是空dict."""
snap = {"config": None}
result = snapshot_to_template_clip_config("t1", snap)
assert result.config == {}
def test_config_is_copy(self):
"""config是副本."""
original_config = {"a": 1}
snap = {"config": original_config}
result = snapshot_to_template_clip_config("t1", snap)
result.config["b"] = 2
assert "b" not in original_config
class TestSnapshotsToTemplateClipConfigs:
"""snapshots_to_template_clip_configs 批量测试."""
def test_empty_list(self):
"""空列表返回空列表."""
result = snapshots_to_template_clip_configs("t1", [])
assert result == []
def test_multiple_snapshots(self):
"""多个快照转换."""
snaps = [
{"clip_type": "intro", "order": 0, "min_duration": 2.0, "max_duration": 2.0},
{"clip_type": "main", "order": 1, "min_duration": 5.0, "max_duration": 5.0},
{"clip_type": "outro", "order": 2, "min_duration": 3.0, "max_duration": 3.0},
]
result = snapshots_to_template_clip_configs("tpl_multi", snaps)
assert len(result) == 3
assert result[0].clip_type == ClipType.INTRO
assert result[1].clip_type == ClipType.MAIN
assert result[2].clip_type == ClipType.OUTRO
assert all(r.template_id == "tpl_multi" for r in result)
# ── validate_template_name 测试 ──────────────────────────────────────────────
class TestValidateTemplateName:
"""validate_template_name 测试."""
def test_normal_name(self):
"""正常名称."""
assert validate_template_name("我的模板") == "我的模板"
def test_strips_whitespace(self):
"""去除首尾空格."""
assert validate_template_name(" 测试模板 ") == "测试模板"
def test_strips_newlines(self):
"""去除换行."""
assert validate_template_name("\n模板名\n") == "模板名"
def test_empty_string_raises(self):
"""空字符串抛错."""
with pytest.raises(ValueError, match="不能为空"):
validate_template_name("")
def test_none_raises(self):
"""None抛错."""
with pytest.raises(ValueError, match="不能为空"):
validate_template_name(None)
def test_whitespace_only_raises(self):
"""纯空白抛错."""
with pytest.raises(ValueError, match="不能为空"):
validate_template_name(" ")
def test_newline_only_raises(self):
"""纯换行抛错."""
with pytest.raises(ValueError, match="不能为空"):
validate_template_name("\n\t\r")
def test_single_char_name(self):
"""单字符名称有效."""
assert validate_template_name("A") == "A"
def test_long_name_preserved(self):
"""长名称保留(长度限制由调用方控制)."""
long_name = "A" * 200
assert validate_template_name(long_name) == long_name
# ── 往返转换测试 ──────────────────────────────────────────────────────────────
class TestRoundTripConversions:
"""往返转换一致性测试."""
def test_snapshot_round_trip(self):
"""snapshot → config → snapshot 数据一致."""
original = {
"clip_type": "main",
"order": 5,
"min_duration": 2.5,
"max_duration": 7.5,
"text_template": "测试文案",
"transition_effect": "dissolve",
"config": {"speed": 1.2, "theme": "dark"},
}
cfg = snapshot_to_template_clip_config("t1", original)
snap = clip_config_to_snapshot(cfg)
assert snap["clip_type"] == original["clip_type"]
assert snap["order"] == original["order"]
assert snap["min_duration"] == original["min_duration"]
assert snap["max_duration"] == original["max_duration"]
assert snap["text_template"] == original["text_template"]
assert snap["transition_effect"] == original["transition_effect"]
assert snap["config"] == original["config"]
def test_config_snapshot_round_trip(self):
"""config → snapshot → config 数据一致."""
cfg = TemplateClipConfig.create(
template_id="t1",
clip_type="title",
order=0,
min_duration=3.0,
max_duration=3.0,
text_template="标题",
transition_effect="wipe",
config={"font_size": 32},
)
snap = clip_config_to_snapshot(cfg)
restored = snapshot_to_template_clip_config("t1", snap)
assert restored.clip_type == cfg.clip_type
assert restored.order == cfg.order
assert restored.min_duration == cfg.min_duration
assert restored.max_duration == cfg.max_duration
assert restored.text_template == cfg.text_template
assert restored.transition_effect == cfg.transition_effect
assert restored.config == cfg.config