"""edit_plan_clip 领域模型单元测试.""" from datetime import datetime, timezone import pytest from domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus class TestEditPlanClipStatus: """EditPlanClipStatus 枚举测试.""" def test_values(self): assert EditPlanClipStatus.PENDING == "pending" assert EditPlanClipStatus.READY == "ready" assert EditPlanClipStatus.RENDERED == "rendered" assert EditPlanClipStatus.FAILED == "failed" class TestEditPlanClipCreate: """EditPlanClip.create 工厂方法测试.""" def test_create_with_required_fields(self): clip = EditPlanClip.create(plan_id="plan_001", clip_type="video", order=1) assert clip.id # 自动生成的 UUID assert len(clip.id) == 32 # hex 格式 assert clip.plan_id == "plan_001" assert clip.clip_type == "video" assert clip.order == 1 assert clip.status == EditPlanClipStatus.PENDING assert clip.start_time == 0.0 assert clip.duration == 0.0 assert clip.transition_effect == "cut" assert clip.playback_speed == 1.0 assert clip.config == {} def test_create_with_all_fields(self): clip = EditPlanClip.create( plan_id="plan_002", clip_type="audio", order=2, template_clip_config_id="tpl_001", asset_id="asset_001", text_content="测试文案", start_time=5.0, duration=10.0, transition_effect="fade", transition_duration=0.5, playback_speed=1.5, config={"key": "value"}, ) assert clip.plan_id == "plan_002" assert clip.clip_type == "audio" assert clip.order == 2 assert clip.template_clip_config_id == "tpl_001" assert clip.asset_id == "asset_001" assert clip.text_content == "测试文案" assert clip.start_time == 5.0 assert clip.duration == 10.0 assert clip.transition_effect == "fade" assert clip.transition_duration == 0.5 assert clip.playback_speed == 1.5 assert clip.config == {"key": "value"} def test_create_strips_strings(self): clip = EditPlanClip.create( plan_id=" plan_003 ", clip_type=" video ", order=1, asset_id=" asset_001 ", template_clip_config_id=" tpl_001 ", text_content=" 测试 ", transition_effect=" fade ", ) assert clip.plan_id == "plan_003" assert clip.clip_type == "video" assert clip.asset_id == "asset_001" assert clip.template_clip_config_id == "tpl_001" assert clip.text_content == "测试" assert clip.transition_effect == "fade" def test_create_empty_plan_id_raises(self): with pytest.raises(ValueError, match="plan_id"): EditPlanClip.create(plan_id="", clip_type="video", order=1) def test_create_whitespace_plan_id_raises(self): with pytest.raises(ValueError, match="plan_id"): EditPlanClip.create(plan_id=" ", clip_type="video", order=1) def test_create_empty_clip_type_raises(self): with pytest.raises(ValueError, match="clip_type"): EditPlanClip.create(plan_id="plan_001", clip_type="", order=1) def test_create_negative_start_time_raises(self): with pytest.raises(ValueError, match="start_time"): EditPlanClip.create(plan_id="p", clip_type="v", order=1, start_time=-1.0) def test_create_negative_duration_raises(self): with pytest.raises(ValueError, match="duration"): EditPlanClip.create(plan_id="p", clip_type="v", order=1, duration=-5.0) def test_create_zero_speed_clamps_to_1(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=0.0) assert clip.playback_speed == 1.0 def test_create_negative_speed_clamps_to_1(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=-1.0) assert clip.playback_speed == 1.0 def test_create_low_speed_clamps_to_min(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=0.1) assert clip.playback_speed == 0.25 def test_create_high_speed_clamps_to_max(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=5.0) assert clip.playback_speed == 4.0 def test_create_speed_at_boundary_values(self): # 边界值应该保持不变 clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=0.25) assert clip.playback_speed == 0.25 clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, playback_speed=4.0) assert clip.playback_speed == 4.0 def test_create_negative_transition_duration_clamps_to_0(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, transition_duration=-1.0) assert clip.transition_duration == 0.0 def test_create_empty_transition_effect_defaults_to_cut(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, transition_effect="") assert clip.transition_effect == "cut" def test_create_empty_asset_id_stays_empty(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, asset_id="") assert clip.asset_id == "" def test_create_none_config_defaults_to_empty_dict(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, config=None) assert clip.config == {} def test_create_ids_are_unique(self): c1 = EditPlanClip.create(plan_id="p", clip_type="v", order=1) c2 = EditPlanClip.create(plan_id="p", clip_type="v", order=2) assert c1.id != c2.id def test_create_timestamps_are_utc(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) assert clip.created_at.tzinfo is not None assert clip.updated_at.tzinfo is not None class TestEditPlanClipStateMachine: """状态机流转测试.""" @pytest.fixture def pending_clip(self): return EditPlanClip.create(plan_id="plan_001", clip_type="video", order=1) def test_initial_status_is_pending(self, pending_clip): assert pending_clip.status == EditPlanClipStatus.PENDING def test_pending_to_ready(self, pending_clip): pending_clip.mark_ready() assert pending_clip.status == EditPlanClipStatus.READY def test_pending_cannot_mark_rendered(self, pending_clip): with pytest.raises(ValueError, match="只有 ready"): pending_clip.mark_rendered() def test_pending_cannot_mark_failed(self, pending_clip): with pytest.raises(ValueError, match="只有 ready"): pending_clip.mark_failed() def test_ready_to_rendered(self, pending_clip): pending_clip.mark_ready() pending_clip.mark_rendered() assert pending_clip.status == EditPlanClipStatus.RENDERED def test_ready_to_failed(self, pending_clip): pending_clip.mark_ready() pending_clip.mark_failed() assert pending_clip.status == EditPlanClipStatus.FAILED def test_rendered_cannot_mark_ready_again(self, pending_clip): pending_clip.mark_ready() pending_clip.mark_rendered() with pytest.raises(ValueError): pending_clip.mark_ready() def test_failed_cannot_mark_ready_again(self, pending_clip): pending_clip.mark_ready() pending_clip.mark_failed() with pytest.raises(ValueError): pending_clip.mark_ready() def test_state_transition_updates_updated_at(self, pending_clip): old_updated = pending_clip.updated_at # 确保时间不同 import time time.sleep(0.001) pending_clip.mark_ready() assert pending_clip.updated_at > old_updated class TestEditPlanClipAssignAsset: """assign_asset 方法测试.""" def test_assign_asset(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) assert not clip.has_asset clip.assign_asset("asset_001") assert clip.asset_id == "asset_001" assert clip.has_asset def test_assign_asset_strips_whitespace(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) clip.assign_asset(" asset_001 ") assert clip.asset_id == "asset_001" def test_assign_empty_asset_raises(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) with pytest.raises(ValueError, match="asset_id"): clip.assign_asset("") def test_assign_whitespace_asset_raises(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) with pytest.raises(ValueError, match="asset_id"): clip.assign_asset(" ") def test_assign_updates_updated_at(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) old_updated = clip.updated_at import time time.sleep(0.001) clip.assign_asset("asset_001") assert clip.updated_at > old_updated class TestEditPlanClipProperties: """属性方法测试.""" def test_end_time(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, start_time=5.0, duration=10.0) assert clip.end_time == 15.0 def test_end_time_zero_duration(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, start_time=3.0, duration=0.0) assert clip.end_time == 3.0 def test_has_asset_true(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, asset_id="a001") assert clip.has_asset is True def test_has_asset_false(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1) assert clip.has_asset is False def test_has_asset_empty_string(self): clip = EditPlanClip.create(plan_id="p", clip_type="v", order=1, asset_id="") assert clip.has_asset is False