"""Domain 小模块合集单元测试。 覆盖零测试的小 domain 模块: - EditingMode 枚举 - Template / TemplateSegment - TemplateClipConfig + ClipType + TransitionEffect - EditTemplateVersion - VoiceLibraryItem - TitleLibraryItem - Recipe / RecipeItem """ from datetime import datetime, timezone import pytest from packages.domain.editing_mode import EditingMode from packages.domain.recipe import RecipeItem from packages.domain.template import TemplateSegment from packages.domain.template_clip_config import ( ClipType, TemplateClipConfig, TransitionEffect, ) from packages.domain.template_version import EditTemplateVersion from packages.domain.title_library import TitleLibraryItem from packages.domain.voice_library import VoiceLibraryItem class TestEditingMode: def test_all_modes_exist(self): assert EditingMode.ONE_TAKE.value == "one_take" assert EditingMode.PIP.value == "pip" assert EditingMode.VOICE_OVER.value == "voice_over" assert EditingMode.VOICE_PIP.value == "voice_pip" def test_from_string(self): assert EditingMode("one_take") == EditingMode.ONE_TAKE assert EditingMode("voice_over") == EditingMode.VOICE_OVER def test_invalid_mode_raises(self): with pytest.raises(ValueError): EditingMode("invalid_mode") def test_is_str_enum(self): # StrEnum 的值是字符串,可以直接比较 assert EditingMode.ONE_TAKE == "one_take" class TestTemplateSegment: def test_create_minimal(self): seg = TemplateSegment( id="seg1", template_id="tpl1", segment_order=1, duration_min=5.0, duration_max=10.0, ) assert seg.id == "seg1" assert seg.template_id == "tpl1" assert seg.segment_order == 1 assert seg.duration_min == 5.0 assert seg.duration_max == 10.0 assert seg.material_type is None assert isinstance(seg.created_at, datetime) def test_create_with_material_type(self): seg = TemplateSegment( id="seg2", template_id="tpl1", segment_order=2, duration_min=3.0, duration_max=8.0, material_type="人物", ) assert seg.material_type == "人物" class TestClipType: def test_basic_types_exist(self): assert hasattr(ClipType, "MAIN") assert hasattr(ClipType, "INTRO") assert hasattr(ClipType, "OUTRO") assert hasattr(ClipType, "TRANSITION") def test_values_are_strings(self): for ct in ClipType: assert isinstance(ct.value, str) class TestTransitionEffect: def test_effects_exist(self): assert TransitionEffect.CUT.value == "cut" assert TransitionEffect.FADE.value == "fade" assert TransitionEffect.DISSOLVE.value == "dissolve" # 至少有 5 种以上转场效果 assert len(list(TransitionEffect)) >= 5 class TestTemplateClipConfig: def test_create_minimal(self): config = TemplateClipConfig.create( template_id="tpl1", clip_type=ClipType.MAIN, order=1, min_duration=3.0, max_duration=8.0, ) assert config.id is not None assert config.template_id == "tpl1" assert config.clip_type == ClipType.MAIN assert config.order == 1 assert config.min_duration == 3.0 assert config.max_duration == 8.0 def test_create_with_string_type(self): config = TemplateClipConfig.create( template_id="tpl1", clip_type="intro", order=0, min_duration=2.0, max_duration=5.0, ) assert config.clip_type == ClipType.INTRO def test_has_duration_range_true(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=3.0, max_duration=8.0, ) assert config.has_duration_range is True def test_has_duration_range_false_when_both_zero(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, ) assert config.has_duration_range is False def test_default_duration_midpoint(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=4.0, max_duration=6.0, ) assert config.default_duration == pytest.approx(5.0) def test_default_duration_when_only_max(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, max_duration=5.0, ) assert config.default_duration == 5.0 def test_create_negative_min_duration_raises(self): with pytest.raises(ValueError, match="min_duration"): TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=-1.0, ) def test_create_min_greater_than_max_raises(self): with pytest.raises(ValueError, match="min_duration.*max_duration"): TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=10.0, max_duration=5.0, ) def test_create_empty_template_id_raises(self): with pytest.raises(ValueError, match="template_id"): TemplateClipConfig.create( template_id="", clip_type=ClipType.MAIN, order=1, ) def test_default_transition_is_cut(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, ) assert config.transition_effect == TransitionEffect.CUT def test_custom_transition_effect(self): config = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, transition_effect="fade", ) assert config.transition_effect == TransitionEffect.FADE class TestEditTemplateVersion: def test_create_minimal(self): version = EditTemplateVersion.create( template_id="tpl1", version=1, ) assert version.id is not None assert len(version.id) == 32 assert version.template_id == "tpl1" assert version.version == 1 assert version.config == {} assert version.clip_configs == [] assert version.published_by == "" assert version.change_note == "" assert version.name == "" assert version.editing_mode == "one_take" assert isinstance(version.created_at, datetime) def test_create_with_config_and_clip_configs(self): version = EditTemplateVersion.create( template_id="tpl1", version=2, config={"layout": "one_take"}, clip_configs=[{"clip_id": "c1", "type": "main"}], published_by="user1", change_note="添加了片头效果", ) assert version.config == {"layout": "one_take"} assert len(version.clip_configs) == 1 assert version.published_by == "user1" assert version.change_note == "添加了片头效果" def test_create_with_name_and_mode(self): version = EditTemplateVersion.create( template_id="t1", version=1, name="v1.0 正式版", editing_mode="voice_over", ) assert version.name == "v1.0 正式版" assert version.editing_mode == "voice_over" def test_create_unique_ids(self): v1 = EditTemplateVersion.create("t1", 1) v2 = EditTemplateVersion.create("t1", 2) assert v1.id != v2.id def test_none_config_defaults_to_empty_dict(self): version = EditTemplateVersion.create("t1", 1, config=None) assert version.config == {} def test_none_clip_configs_defaults_to_empty_list(self): version = EditTemplateVersion.create("t1", 1, clip_configs=None) assert version.clip_configs == [] class TestVoiceLibraryItem: def test_create_minimal(self): item = VoiceLibraryItem( id="v1", user_id="u1", name="我的配音", ) assert item.id == "v1" assert item.user_id == "u1" assert item.name == "我的配音" assert item.text == "" assert item.voice_provider == "" assert item.duration == 0 assert item.status == "completed" assert item.tags == [] assert item.project_id is None assert isinstance(item.created_at, datetime) def test_create_with_all_fields(self): item = VoiceLibraryItem( id="v2", user_id="u1", name="产品介绍", text="欢迎来到我们的产品", voice_provider="cosyvoice", voice_id="voice_001", voice_name="温柔女声", audio_url="https://cdn/v2.mp3", duration=30.5, file_size=102400, status="processing", project_id="proj1", tags=["产品", "介绍"], ) assert item.text == "欢迎来到我们的产品" assert item.voice_provider == "cosyvoice" assert item.voice_id == "voice_001" assert item.audio_url == "https://cdn/v2.mp3" assert item.duration == 30.5 assert item.file_size == 102400 assert item.status == "processing" assert item.project_id == "proj1" assert item.tags == ["产品", "介绍"] class TestTitleLibraryItem: def test_create_minimal(self): item = TitleLibraryItem( id="t1", user_id="u1", name="爆款标题1", text="这是一个爆款标题", ) assert item.id == "t1" assert item.user_id == "u1" assert item.name == "爆款标题1" assert item.text == "这是一个爆款标题" assert item.category == "default" assert item.description == "" assert item.tags == [] assert item.usage_count == 0 assert item.is_active is True def test_create_with_category(self): item = TitleLibraryItem( id="t2", user_id="u1", name="美食标题", text="太好吃了!", category="美食", ) assert item.category == "美食" def test_inactive_item(self): item = TitleLibraryItem( id="t3", user_id="u1", name="旧标题", text="旧文案", is_active=False, ) assert item.is_active is False def test_usage_count_increment(self): item = TitleLibraryItem( id="t4", user_id="u1", name="T", text="T", ) item.usage_count += 1 assert item.usage_count == 1 class TestRecipeItem: def test_create_minimal(self): item = RecipeItem( id="ri1", recipe_id="r1", item_type="asset", item_id="asset_001", ) assert item.id == "ri1" assert item.recipe_id == "r1" assert item.item_type == "asset" assert item.item_id == "asset_001" assert item.position == 0 assert item.metadata_ == {} def test_create_with_position_and_metadata(self): item = RecipeItem( id="ri2", recipe_id="r1", item_type="title", item_id="title_001", position=2, metadata_={"style": "bold"}, ) assert item.position == 2 assert item.metadata_ == {"style": "bold"} def test_item_types_variety(self): asset_item = RecipeItem(id="a", recipe_id="r", item_type="asset", item_id="i1") title_item = RecipeItem(id="t", recipe_id="r", item_type="title", item_id="i2") voice_item = RecipeItem(id="v", recipe_id="r", item_type="voice", item_id="i3") assert asset_item.item_type == "asset" assert title_item.item_type == "title" assert voice_item.item_type == "voice"