From ac01bee33b3df8d049bf9a06aaea48dffae34b72 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 26 Jul 2026 17:43:20 +0800 Subject: [PATCH] test(wave99): 35 unit tests for small domain modules Clean up zero-test small domain modules in one batch: - EditingMode enum: 4 tests - TemplateSegment dataclass: 2 tests - ClipType + TransitionEffect enums: 3 tests - TemplateClipConfig create + properties: 11 tests (validation + duration range + transition) - EditTemplateVersion create: 6 tests - VoiceLibraryItem: 2 tests - TitleLibraryItem: 4 tests - RecipeItem: 3 tests Total: 35 tests across 8 modules --- tests/unit/test_domain_small_modules.py | 391 ++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100755 tests/unit/test_domain_small_modules.py diff --git a/tests/unit/test_domain_small_modules.py b/tests/unit/test_domain_small_modules.py new file mode 100755 index 000000000..ab018f167 --- /dev/null +++ b/tests/unit/test_domain_small_modules.py @@ -0,0 +1,391 @@ +"""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"