From 50f81909fd030535bbac59947182bab2b92d6b7e Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 24 Jul 2026 18:14:34 +0800 Subject: [PATCH] =?UTF-8?q?test:=20P3-1=20=E7=AC=AC50=E6=B3=A2=E5=8D=95?= =?UTF-8?q?=E5=85=83=E6=B5=8B=E8=AF=95=EF=BC=88generation=5Ftask/edit=5Fpl?= =?UTF-8?q?an/edit=5Ftemplate/edit=5Fplan=5Fclip=EF=BC=8C+86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增4个领域模块单测: - test_generation_task.py: GenerationTask生成任务领域模型(32个) - create参数校验 + 字段strip - 状态查询(is_terminal/is_completed/is_failed/is_running) - 状态流转(pending→running→completed/failed,重试回滚) - 日志系统(append_log/get_logs,200条上限截断) - 非法状态转换校验 - test_edit_plan.py: EditPlan剪辑计划实体(11个) - test_edit_template.py: EditTemplate剪辑模板实体(12个) - editing_mode枚举校验 - test_edit_plan_clip.py: EditPlanClip剪辑片段(30个) - create参数校验 + playback_speed边界钳制 - 状态流转(pending→ready→rendered/failed) - end_time/has_asset属性 + assign_asset方法 合计 +86 个测试用例,全部通过。 --- tests/unit/test_edit_plan.py | 78 ++++++++ tests/unit/test_edit_plan_clip.py | 181 +++++++++++++++++ tests/unit/test_edit_template.py | 68 +++++++ tests/unit/test_generation_task.py | 300 +++++++++++++++++++++++++++++ 4 files changed, 627 insertions(+) create mode 100755 tests/unit/test_edit_plan.py create mode 100755 tests/unit/test_edit_plan_clip.py create mode 100755 tests/unit/test_edit_template.py create mode 100755 tests/unit/test_generation_task.py diff --git a/tests/unit/test_edit_plan.py b/tests/unit/test_edit_plan.py new file mode 100755 index 000000000..11a6d4895 --- /dev/null +++ b/tests/unit/test_edit_plan.py @@ -0,0 +1,78 @@ +"""EditPlan 剪辑计划领域实体单测.""" + +import pytest + +from packages.domain.edit_plan import EditPlan, EditPlanStatus + + +class TestEditPlanStatus: + def test_values(self): + assert EditPlanStatus.DRAFT.value == "draft" + assert EditPlanStatus.EDITING.value == "editing" + assert EditPlanStatus.RENDERING.value == "rendering" + assert EditPlanStatus.COMPLETED.value == "completed" + assert EditPlanStatus.FAILED.value == "failed" + + def test_is_str(self): + assert isinstance(EditPlanStatus.DRAFT, str) + + +class TestEditPlanCreate: + def test_create_normal(self): + plan = EditPlan.create(template_id="tmpl1", name="我的剪辑计划") + assert plan.id + assert plan.template_id == "tmpl1" + assert plan.name == "我的剪辑计划" + assert plan.status == EditPlanStatus.DRAFT + assert plan.total_duration == 0.0 + assert plan.config == {} + + def test_create_strips_whitespace(self): + plan = EditPlan.create( + template_id=" tmpl1 ", + name=" 我的计划 ", + source_edit_plan_id=" src1 ", + project_id=" proj1 ", + created_by_user_id=" user1 ", + ) + assert plan.template_id == "tmpl1" + assert plan.name == "我的计划" + assert plan.source_edit_plan_id == "src1" + assert plan.project_id == "proj1" + assert plan.created_by_user_id == "user1" + + def test_create_empty_name_raises(self): + with pytest.raises(ValueError, match="计划名称不能为空"): + EditPlan.create(template_id="tmpl1", name="") + + def test_create_whitespace_name_raises(self): + with pytest.raises(ValueError, match="计划名称不能为空"): + EditPlan.create(template_id="tmpl1", name=" ") + + def test_create_empty_template_id_raises(self): + with pytest.raises(ValueError, match="template_id 不能为空"): + EditPlan.create(template_id="", name="计划") + + def test_create_whitespace_template_id_raises(self): + with pytest.raises(ValueError, match="template_id 不能为空"): + EditPlan.create(template_id=" ", name="计划") + + def test_create_with_config(self): + config = {"resolution": "1080p", "fps": 30} + plan = EditPlan.create( + template_id="tmpl1", + name="计划", + config=config, + total_duration=30.5, + ) + assert plan.config == config + assert plan.total_duration == 30.5 + + def test_create_none_config_defaults_to_empty(self): + plan = EditPlan.create(template_id="tmpl1", name="计划", config=None) + assert plan.config == {} + + def test_create_unique_ids(self): + p1 = EditPlan.create(template_id="t1", name="p1") + p2 = EditPlan.create(template_id="t1", name="p2") + assert p1.id != p2.id diff --git a/tests/unit/test_edit_plan_clip.py b/tests/unit/test_edit_plan_clip.py new file mode 100755 index 000000000..81e683e0c --- /dev/null +++ b/tests/unit/test_edit_plan_clip.py @@ -0,0 +1,181 @@ +"""EditPlanClip 剪辑计划片段领域实体单测.""" + +import pytest + +from packages.domain.edit_plan_clip import EditPlanClip, EditPlanClipStatus + + +class TestEditPlanClipStatus: + def test_values(self): + assert EditPlanClipStatus.PENDING.value == "pending" + assert EditPlanClipStatus.READY.value == "ready" + assert EditPlanClipStatus.RENDERED.value == "rendered" + assert EditPlanClipStatus.FAILED.value == "failed" + + +class TestEditPlanClipCreate: + def test_create_normal(self): + clip = EditPlanClip.create( + plan_id="plan1", + clip_type="video", + order=1, + start_time=0.0, + duration=5.0, + ) + assert clip.id + assert clip.plan_id == "plan1" + assert clip.clip_type == "video" + assert clip.order == 1 + assert clip.status == EditPlanClipStatus.PENDING + assert clip.start_time == 0.0 + assert clip.duration == 5.0 + assert clip.playback_speed == 1.0 + assert clip.config == {} + + 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_empty_clip_type_raises(self): + with pytest.raises(ValueError, match="clip_type 不能为空"): + EditPlanClip.create(plan_id="p1", clip_type="", order=1) + + def test_create_negative_start_time_raises(self): + with pytest.raises(ValueError, match="start_time 不能为负数"): + EditPlanClip.create(plan_id="p1", 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="p1", clip_type="v", order=1, duration=-1.0) + + def test_create_strips_strings(self): + clip = EditPlanClip.create( + plan_id=" plan1 ", + clip_type=" video ", + order=1, + template_clip_config_id=" cfg1 ", + asset_id=" a1 ", + text_content=" 你好 ", + transition_effect=" fade ", + ) + assert clip.plan_id == "plan1" + assert clip.clip_type == "video" + assert clip.template_clip_config_id == "cfg1" + assert clip.asset_id == "a1" + assert clip.text_content == "你好" + assert clip.transition_effect == "fade" + + def test_create_empty_transition_effect_defaults_to_cut(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, transition_effect="") + assert clip.transition_effect == "cut" + + def test_create_playback_speed_zero_defaults_to_1(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, playback_speed=0) + assert clip.playback_speed == 1.0 + + def test_create_playback_speed_negative_defaults_to_1(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, playback_speed=-1.0) + assert clip.playback_speed == 1.0 + + def test_create_playback_speed_below_min_clamped(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, playback_speed=0.1) + assert clip.playback_speed == 0.25 + + def test_create_playback_speed_above_max_clamped(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, playback_speed=5.0) + assert clip.playback_speed == 4.0 + + def test_create_playback_speed_within_range(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, playback_speed=1.5) + assert clip.playback_speed == 1.5 + + def test_create_transition_duration_negative_clamped(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, transition_duration=-0.5) + assert clip.transition_duration == 0.0 + + def test_create_none_config_defaults_to_empty(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, config=None) + assert clip.config == {} + + def test_create_unique_ids(self): + c1 = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + c2 = EditPlanClip.create(plan_id="p1", clip_type="v", order=2) + assert c1.id != c2.id + + +class TestEditPlanClipStateTransitions: + def test_pending_to_ready(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.mark_ready() + assert clip.status == EditPlanClipStatus.READY + + def test_ready_to_rendered(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.mark_ready() + clip.mark_rendered() + assert clip.status == EditPlanClipStatus.RENDERED + + def test_ready_to_failed(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.mark_ready() + clip.mark_failed() + assert clip.status == EditPlanClipStatus.FAILED + + def test_ready_mark_ready_raises(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.mark_ready() + with pytest.raises(ValueError, match="只有 pending 状态的片段可以标记就绪"): + clip.mark_ready() + + def test_pending_mark_rendered_raises(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + with pytest.raises(ValueError, match="只有 ready 状态的片段可以标记已渲染"): + clip.mark_rendered() + + def test_pending_mark_failed_raises(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + with pytest.raises(ValueError, match="只有 ready 状态的片段可以标记失败"): + clip.mark_failed() + + +class TestEditPlanClipProperties: + def test_end_time(self): + clip = EditPlanClip.create( + plan_id="p1", clip_type="v", order=1, start_time=10.0, duration=5.0 + ) + assert clip.end_time == 15.0 + + def test_end_time_zero_duration(self): + clip = EditPlanClip.create( + plan_id="p1", clip_type="v", order=1, start_time=5.0, duration=0.0 + ) + assert clip.end_time == 5.0 + + def test_has_asset_true(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, asset_id="a1") + assert clip.has_asset is True + + def test_has_asset_false(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + assert clip.has_asset is False + + def test_has_asset_empty_string(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1, asset_id="") + assert clip.has_asset is False + + +class TestEditPlanClipAssignAsset: + def test_assign_asset(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.assign_asset("asset1") + assert clip.asset_id == "asset1" + + def test_assign_asset_strips(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + clip.assign_asset(" asset1 ") + assert clip.asset_id == "asset1" + + def test_assign_asset_empty_raises(self): + clip = EditPlanClip.create(plan_id="p1", clip_type="v", order=1) + with pytest.raises(ValueError, match="asset_id 不能为空"): + clip.assign_asset("") diff --git a/tests/unit/test_edit_template.py b/tests/unit/test_edit_template.py new file mode 100755 index 000000000..56260e035 --- /dev/null +++ b/tests/unit/test_edit_template.py @@ -0,0 +1,68 @@ +"""EditTemplate 剪辑模板领域实体单测.""" + +import pytest + +from packages.domain.edit_template import EditTemplate, EditTemplateStatus +from packages.domain.editing_mode import EditingMode + + +class TestEditTemplateStatus: + def test_values(self): + assert EditTemplateStatus.ACTIVE.value == "active" + assert EditTemplateStatus.INACTIVE.value == "inactive" + + +class TestEditTemplateCreate: + def test_create_default(self): + tpl = EditTemplate.create(name="测试模板") + assert tpl.id + assert tpl.name == "测试模板" + assert tpl.editing_mode == EditingMode.ONE_TAKE.value + assert tpl.status == EditTemplateStatus.ACTIVE + assert tpl.version == 1 + assert tpl.config == {} + assert tpl.description == "" + assert tpl.sort_weight == 0 + + def test_create_strips_name(self): + tpl = EditTemplate.create(name=" 我的模板 ") + assert tpl.name == "我的模板" + + def test_create_empty_name_raises(self): + with pytest.raises(ValueError, match="模板名称不能为空"): + EditTemplate.create(name="") + + def test_create_whitespace_name_raises(self): + with pytest.raises(ValueError, match="模板名称不能为空"): + EditTemplate.create(name=" ") + + def test_create_valid_editing_modes(self): + for mode in EditingMode: + tpl = EditTemplate.create(name=f"模板_{mode.value}", editing_mode=mode.value) + assert tpl.editing_mode == mode.value + + def test_create_invalid_editing_mode_raises(self): + with pytest.raises(ValueError, match="无效的 editing_mode"): + EditTemplate.create(name="模板", editing_mode="invalid_mode") + + def test_create_empty_editing_mode_defaults_to_one_take(self): + tpl = EditTemplate.create(name="模板", editing_mode="") + assert tpl.editing_mode == EditingMode.ONE_TAKE.value + + def test_create_with_config(self): + config = {"key": "value"} + tpl = EditTemplate.create(name="模板", config=config) + assert tpl.config == config + + def test_create_none_config_defaults_to_empty(self): + tpl = EditTemplate.create(name="模板", config=None) + assert tpl.config == {} + + def test_create_with_custom_status(self): + tpl = EditTemplate.create(name="模板", status=EditTemplateStatus.INACTIVE) + assert tpl.status == EditTemplateStatus.INACTIVE + + def test_create_unique_ids(self): + t1 = EditTemplate.create(name="t1") + t2 = EditTemplate.create(name="t2") + assert t1.id != t2.id diff --git a/tests/unit/test_generation_task.py b/tests/unit/test_generation_task.py new file mode 100755 index 000000000..96213144c --- /dev/null +++ b/tests/unit/test_generation_task.py @@ -0,0 +1,300 @@ +"""GenerationTask 生成任务领域模型单测.""" + +import pytest + +from packages.domain.generation_task import ( + TERMINAL_STATUSES, + GenerationTask, + GenerationTaskStatus, +) + + +class TestGenerationTaskCreate: + def test_create_with_template_id(self): + task = GenerationTask.create( + project_id="", + asset_library_id="lib1", + template_id="tmpl1", + ) + assert task.id + assert task.template_id == "tmpl1" + assert task.asset_library_id == "lib1" + assert task.status == GenerationTaskStatus.PENDING + assert task.progress == 0.0 + assert task.retry_count == 0 + assert task.auto_retry_enabled is False + + def test_create_with_project_id(self): + task = GenerationTask.create( + project_id="proj1", + asset_library_id="lib1", + ) + assert task.project_id == "proj1" + + def test_create_both_empty_raises(self): + with pytest.raises(ValueError, match="project_id 或 template_id 至少需要提供一个"): + GenerationTask.create( + project_id="", + asset_library_id="lib1", + template_id="", + ) + + def test_create_asset_library_and_assets_both_empty_raises(self): + with pytest.raises(ValueError, match="asset_library_id 或 asset_ids/title_ids/voice_ids 至少需要提供一个"): + GenerationTask.create( + project_id="proj1", + asset_library_id="", + asset_ids=None, + title_ids=None, + voice_ids=None, + ) + + def test_create_with_asset_ids(self): + task = GenerationTask.create( + project_id="proj1", + asset_library_id="", + asset_ids=["a1", "a2"], + ) + assert task.asset_ids == ["a1", "a2"] + + def test_create_strips_whitespace(self): + task = GenerationTask.create( + project_id=" proj1 ", + asset_library_id=" lib1 ", + template_id=" tmpl1 ", + video_title=" 测试视频 ", + resolution=" 1080p ", + created_by_user_id=" user1 ", + source_edit_plan_id=" plan1 ", + ) + assert task.project_id == "proj1" + assert task.asset_library_id == "lib1" + assert task.template_id == "tmpl1" + assert task.video_title == "测试视频" + assert task.resolution == "1080p" + assert task.created_by_user_id == "user1" + assert task.source_edit_plan_id == "plan1" + + def test_create_default_values(self): + task = GenerationTask.create( + project_id="proj1", + asset_library_id="lib1", + ) + assert task.title_ids == [] + assert task.voice_ids == [] + assert task.result_count == 0 + assert task.error_message == "" + assert task.error_info == {} + assert task.bgm_config == {} + assert task.logs == "[]" + + def test_create_unique_ids(self): + t1 = GenerationTask.create(project_id="p1", asset_library_id="l1") + t2 = GenerationTask.create(project_id="p1", asset_library_id="l1") + assert t1.id != t2.id + + +class TestGenerationTaskStatusQueries: + def test_is_terminal_completed(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_completed() + assert task.is_terminal is True + assert task.is_completed is True + assert task.is_failed is False + assert task.is_running is False + + def test_is_terminal_failed(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_failed("error") + assert task.is_terminal is True + assert task.is_completed is False + assert task.is_failed is True + + def test_is_terminal_cancelled(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.transition_to(GenerationTaskStatus.CANCELLED) + assert task.is_terminal is True + + def test_is_running(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + assert task.is_running is False + task.mark_processing() + assert task.is_running is True + + def test_terminal_statuses_set(self): + assert GenerationTaskStatus.COMPLETED in TERMINAL_STATUSES + assert GenerationTaskStatus.FAILED in TERMINAL_STATUSES + assert GenerationTaskStatus.CANCELLED in TERMINAL_STATUSES + assert GenerationTaskStatus.PENDING not in TERMINAL_STATUSES + assert GenerationTaskStatus.RUNNING not in TERMINAL_STATUSES + + +class TestGenerationTaskStateTransitions: + def test_pending_to_running(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + assert task.status == GenerationTaskStatus.RUNNING + assert task.started_at is not None + assert task.error_message == "" + + def test_running_to_completed(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_completed(result_count=3) + assert task.status == GenerationTaskStatus.COMPLETED + assert task.completed_at is not None + assert task.progress == 100.0 + assert task.result_count == 3 + assert task.error_message == "" + + def test_running_to_failed_with_error_info(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_failed("渲染失败", error_info={"stage": "render"}) + assert task.status == GenerationTaskStatus.FAILED + assert task.completed_at is not None + assert task.error_message == "渲染失败" + assert task.error_info["stage"] == "render" + + def test_running_to_failed_without_error_info(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_failed("未知错误") + assert task.error_info is not None + assert task.error_info["message"] == "未知错误" + assert "error_type" in task.error_info + assert "failed_at" in task.error_info + + def test_failed_to_pending_retry(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_failed("error") + assert task.retry_count == 0 + + task.mark_pending_from_failed() + assert task.status == GenerationTaskStatus.PENDING + assert task.retry_count == 1 + assert task.error_message == "" + assert task.error_info == {} + assert task.started_at is None + assert task.completed_at is None + assert task.progress == 0.0 + assert task.result_count == 0 + + def test_mark_pending_from_failed_wrong_status_raises(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + with pytest.raises(ValueError, match="只有 failed 状态的任务可以重置为 pending"): + task.mark_pending_from_failed() + + def test_invalid_transition_raises(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + # pending 不能直接到 completed + with pytest.raises(ValueError, match="非法状态转换"): + task.transition_to(GenerationTaskStatus.COMPLETED) + + def test_completed_cannot_transition(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_completed() + with pytest.raises(ValueError, match="非法状态转换"): + task.mark_failed("test") + + def test_transition_to_with_string(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.transition_to("running") + assert task.status == GenerationTaskStatus.RUNNING + + def test_transition_to_invalid_string_raises(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + with pytest.raises(ValueError, match="无效状态"): + task.transition_to("invalid_status") + + +class TestGenerationTaskLogs: + def test_append_log_single(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.append_log("初始化", "任务创建成功") + logs = task.get_logs() + assert len(logs) == 1 + assert logs[0]["stage"] == "初始化" + assert logs[0]["message"] == "任务创建成功" + assert logs[0]["level"] == "INFO" + assert "ts" in logs[0] + + def test_append_log_multiple(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + for i in range(5): + task.append_log(f"stage{i}", f"msg{i}", level="INFO") + logs = task.get_logs() + assert len(logs) == 5 + assert logs[0]["stage"] == "stage0" + assert logs[4]["stage"] == "stage4" + + def test_append_log_with_extra_fields(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.append_log("下载", "下载完成", asset_id="a1", duration=10.5) + logs = task.get_logs() + assert logs[0]["asset_id"] == "a1" + assert logs[0]["duration"] == 10.5 + + def test_append_log_error_level(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.append_log("渲染", "渲染失败", level="ERROR") + logs = task.get_logs() + assert logs[0]["level"] == "ERROR" + + def test_logs_max_limit(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + # _MAX_LOGS = 200 + for i in range(250): + task.append_log("test", f"msg{i}") + logs = task.get_logs() + assert len(logs) == 200 + # 保留最新的200条 + assert logs[0]["message"] == "msg50" + assert logs[-1]["message"] == "msg249" + + def test_get_logs_empty(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + assert task.get_logs() == [] + + def test_get_logs_corrupted_json(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.logs = "not json" + assert task.get_logs() == [] + + def test_mark_failed_without_error_info(self): + task = GenerationTask.create(project_id="p1", asset_library_id="l1") + task.mark_processing() + task.mark_failed("error msg") + assert task.error_info is not None + assert task.error_info["message"] == "error msg" + assert "error_type" in task.error_info + assert "failed_at" in task.error_info + + +class TestGenerationTaskCreateWithStrategy: + def test_create_with_strategy_and_voice(self): + task = GenerationTask.create( + project_id="p1", + asset_library_id="l1", + strategy_id="s1", + voice_library_id="v1", + auto_retry_enabled=True, + auto_retry_max=3, + ) + assert task.strategy_id == "s1" + assert task.voice_library_id == "v1" + assert task.auto_retry_enabled is True + assert task.auto_retry_max == 3 + + def test_create_with_bgm_config(self): + bgm = {"volume": 0.5, "track": "bgm1"} + task = GenerationTask.create( + project_id="p1", + asset_library_id="l1", + bgm_config=bgm, + ) + assert task.bgm_config == bgm -- 2.54.0