"""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