Files
xiaoxia-saas/tests/unit/test_generation_task.py
xiaoxia 30457629da
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Successful in 4m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m6s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 55s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m6s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m34s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m29s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m50s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
fix(ci): fix preview deploy frontend build - remove DooD, use direct runner (#919)
2026-07-26 12:58:22 +08:00

301 lines
11 KiB
Python
Executable File

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