a78e8c3480
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
321 lines
10 KiB
Python
Executable File
321 lines
10 KiB
Python
Executable File
"""
|
|
生成任务应用层用例单元测试(第十九波)
|
|
|
|
覆盖:
|
|
- CreateGenerationTaskUseCase
|
|
- GetGenerationTaskUseCase
|
|
- ListUserTasksFilteredUseCase
|
|
- RetryGenerationTaskUseCase
|
|
- Command / Filter / Result 对象
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from packages.application.generation_tasks import (
|
|
CreateGenerationTaskCommand,
|
|
CreateGenerationTaskUseCase,
|
|
GetGenerationTaskUseCase,
|
|
ListGenerationTasksResult,
|
|
ListTasksFilter,
|
|
ListUserTasksFilteredUseCase,
|
|
RetryGenerationTaskUseCase,
|
|
)
|
|
from packages.domain.generation_task import GenerationTask, GenerationTaskStatus
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_repo():
|
|
return MagicMock()
|
|
|
|
|
|
def make_task(status=GenerationTaskStatus.PENDING, **kwargs):
|
|
task = GenerationTask(
|
|
id="task-1",
|
|
project_id="proj-1",
|
|
asset_library_id="lib-1",
|
|
strategy_id="strat-1",
|
|
template_id="tmpl-1",
|
|
asset_ids=["asset-1"],
|
|
title_ids=["title-1"],
|
|
voice_ids=["voice-1"],
|
|
created_by_user_id="user-1",
|
|
video_title="测试标题",
|
|
)
|
|
if status != GenerationTaskStatus.PENDING:
|
|
object.__setattr__(task, "status", status)
|
|
# 应用额外 kwargs
|
|
for k, v in kwargs.items():
|
|
object.__setattr__(task, k, v)
|
|
return task
|
|
|
|
|
|
# ============================================================
|
|
# CreateGenerationTaskUseCase
|
|
# ============================================================
|
|
|
|
|
|
class TestCreateGenerationTaskUseCase:
|
|
"""CreateGenerationTaskUseCase 创建生成任务"""
|
|
|
|
def test_create_success(self, mock_repo):
|
|
"""正常创建任务"""
|
|
mock_repo.create.side_effect = lambda t: t
|
|
|
|
cmd = CreateGenerationTaskCommand(
|
|
project_id="proj-1",
|
|
asset_library_id="lib-1",
|
|
strategy_id="strat-1",
|
|
voice_library_id="vlib-1",
|
|
template_id="tmpl-1",
|
|
asset_ids=["a1", "a2"],
|
|
title_ids=["t1"],
|
|
voice_ids=["v1"],
|
|
created_by_user_id="user-1",
|
|
source_edit_plan_id="plan-1",
|
|
asset_select_mode="auto",
|
|
batch_id="batch-1",
|
|
video_title="我的视频",
|
|
auto_retry_enabled=True,
|
|
auto_retry_max=3,
|
|
)
|
|
uc = CreateGenerationTaskUseCase(mock_repo)
|
|
task = uc.execute(cmd)
|
|
|
|
assert task.project_id == "proj-1"
|
|
assert task.asset_library_id == "lib-1"
|
|
assert task.strategy_id == "strat-1"
|
|
assert task.voice_library_id == "vlib-1"
|
|
assert task.template_id == "tmpl-1"
|
|
assert task.asset_ids == ["a1", "a2"]
|
|
assert task.title_ids == ["t1"]
|
|
assert task.voice_ids == ["v1"]
|
|
assert task.created_by_user_id == "user-1"
|
|
assert task.source_edit_plan_id == "plan-1"
|
|
assert task.asset_select_mode == "auto"
|
|
assert task.batch_id == "batch-1"
|
|
assert task.video_title == "我的视频"
|
|
assert task.auto_retry_enabled is True
|
|
assert task.auto_retry_max == 3
|
|
assert task.status == GenerationTaskStatus.PENDING
|
|
assert task.progress == 0.0
|
|
assert task.result_count == 0
|
|
mock_repo.create.assert_called_once()
|
|
|
|
def test_create_default_values(self, mock_repo):
|
|
"""默认参数值"""
|
|
mock_repo.create.side_effect = lambda t: t
|
|
|
|
cmd = CreateGenerationTaskCommand(
|
|
project_id="proj-1",
|
|
asset_library_id="lib-1",
|
|
)
|
|
uc = CreateGenerationTaskUseCase(mock_repo)
|
|
task = uc.execute(cmd)
|
|
|
|
assert task.asset_ids == []
|
|
assert task.title_ids == []
|
|
assert task.voice_ids == []
|
|
assert task.created_by_user_id == ""
|
|
assert task.video_title == ""
|
|
assert task.auto_retry_enabled is False
|
|
assert task.auto_retry_max == 0
|
|
|
|
def test_create_id_is_generated(self, mock_repo):
|
|
"""ID 会自动生成"""
|
|
mock_repo.create.side_effect = lambda t: t
|
|
|
|
cmd = CreateGenerationTaskCommand(project_id="proj-1", asset_library_id="lib-1")
|
|
uc = CreateGenerationTaskUseCase(mock_repo)
|
|
task = uc.execute(cmd)
|
|
|
|
assert task.id
|
|
assert isinstance(task.id, str)
|
|
assert len(task.id) > 10 # uuid hex
|
|
|
|
|
|
# ============================================================
|
|
# GetGenerationTaskUseCase
|
|
# ============================================================
|
|
|
|
|
|
class TestGetGenerationTaskUseCase:
|
|
"""GetGenerationTaskUseCase 获取任务"""
|
|
|
|
def test_get_existing(self, mock_repo):
|
|
"""获取存在的任务"""
|
|
task = make_task()
|
|
mock_repo.get.return_value = task
|
|
|
|
uc = GetGenerationTaskUseCase(mock_repo)
|
|
result = uc.execute("task-1")
|
|
|
|
assert result is task
|
|
mock_repo.get.assert_called_once_with("task-1")
|
|
|
|
def test_get_not_found(self, mock_repo):
|
|
"""获取不存在的任务返回 None"""
|
|
mock_repo.get.return_value = None
|
|
|
|
uc = GetGenerationTaskUseCase(mock_repo)
|
|
result = uc.execute("nonexistent")
|
|
|
|
assert result is None
|
|
|
|
|
|
# ============================================================
|
|
# ListUserTasksFilteredUseCase
|
|
# ============================================================
|
|
|
|
|
|
class TestListUserTasksFilteredUseCase:
|
|
"""ListUserTasksFilteredUseCase 按用户筛选任务"""
|
|
|
|
def test_list_without_filters(self, mock_repo):
|
|
"""无筛选条件查询"""
|
|
tasks = [make_task(), make_task()]
|
|
mock_repo.list_by_user_filtered.return_value = tasks
|
|
mock_repo.count_by_user_filtered.return_value = 2
|
|
|
|
uc = ListUserTasksFilteredUseCase(mock_repo)
|
|
result = uc.execute("user-1")
|
|
|
|
assert isinstance(result, ListGenerationTasksResult)
|
|
assert len(result.items) == 2
|
|
assert result.total == 2
|
|
mock_repo.list_by_user_filtered.assert_called_once_with("user-1", status=None, limit=None, offset=0)
|
|
mock_repo.count_by_user_filtered.assert_called_once_with("user-1", status=None)
|
|
|
|
def test_list_with_status_filter(self, mock_repo):
|
|
"""按状态筛选"""
|
|
mock_repo.list_by_user_filtered.return_value = []
|
|
mock_repo.count_by_user_filtered.return_value = 0
|
|
|
|
uc = ListUserTasksFilteredUseCase(mock_repo)
|
|
uc.execute("user-1", status="running")
|
|
|
|
mock_repo.list_by_user_filtered.assert_called_once_with("user-1", status="running", limit=None, offset=0)
|
|
mock_repo.count_by_user_filtered.assert_called_once_with("user-1", status="running")
|
|
|
|
def test_list_with_pagination(self, mock_repo):
|
|
"""分页查询"""
|
|
mock_repo.list_by_user_filtered.return_value = []
|
|
mock_repo.count_by_user_filtered.return_value = 100
|
|
|
|
uc = ListUserTasksFilteredUseCase(mock_repo)
|
|
result = uc.execute("user-1", limit=10, offset=20)
|
|
|
|
assert result.total == 100
|
|
mock_repo.list_by_user_filtered.assert_called_once_with("user-1", status=None, limit=10, offset=20)
|
|
|
|
def test_list_empty_result(self, mock_repo):
|
|
"""空结果"""
|
|
mock_repo.list_by_user_filtered.return_value = []
|
|
mock_repo.count_by_user_filtered.return_value = 0
|
|
|
|
uc = ListUserTasksFilteredUseCase(mock_repo)
|
|
result = uc.execute("user-1", status="failed")
|
|
|
|
assert result.items == []
|
|
assert result.total == 0
|
|
|
|
|
|
# ============================================================
|
|
# RetryGenerationTaskUseCase
|
|
# ============================================================
|
|
|
|
|
|
class TestRetryGenerationTaskUseCase:
|
|
"""RetryGenerationTaskUseCase 重试失败任务"""
|
|
|
|
def test_retry_success(self, mock_repo):
|
|
"""失败任务重试成功"""
|
|
task = make_task(
|
|
status=GenerationTaskStatus.FAILED,
|
|
error_message="网络超时",
|
|
retry_count=0,
|
|
)
|
|
mock_repo.get.return_value = task
|
|
mock_repo.update.side_effect = lambda t: t
|
|
|
|
uc = RetryGenerationTaskUseCase(mock_repo)
|
|
result = uc.execute("task-1")
|
|
|
|
assert result.status == GenerationTaskStatus.PENDING
|
|
assert result.retry_count == 1
|
|
assert result.error_message == ""
|
|
assert result.error_info == {}
|
|
assert result.progress == 0.0
|
|
assert result.result_count == 0
|
|
assert result.started_at is None
|
|
assert result.completed_at is None
|
|
mock_repo.update.assert_called_once()
|
|
|
|
def test_retry_not_found(self, mock_repo):
|
|
"""任务不存在"""
|
|
mock_repo.get.return_value = None
|
|
|
|
uc = RetryGenerationTaskUseCase(mock_repo)
|
|
with pytest.raises(ValueError, match="任务不存在"):
|
|
uc.execute("nonexistent")
|
|
|
|
def test_retry_not_failed(self, mock_repo):
|
|
"""非失败状态不能重试"""
|
|
task = make_task(status=GenerationTaskStatus.RUNNING)
|
|
mock_repo.get.return_value = task
|
|
|
|
uc = RetryGenerationTaskUseCase(mock_repo)
|
|
with pytest.raises(ValueError, match="只有失败状态"):
|
|
uc.execute("task-1")
|
|
|
|
def test_retry_pending_not_allowed(self, mock_repo):
|
|
"""pending 状态不能重试"""
|
|
task = make_task(status=GenerationTaskStatus.PENDING)
|
|
mock_repo.get.return_value = task
|
|
|
|
uc = RetryGenerationTaskUseCase(mock_repo)
|
|
with pytest.raises(ValueError, match="只有失败状态"):
|
|
uc.execute("task-1")
|
|
|
|
def test_retry_preserves_id(self, mock_repo):
|
|
"""重试复用同一个 task_id"""
|
|
task = make_task(status=GenerationTaskStatus.FAILED)
|
|
original_id = task.id
|
|
mock_repo.get.return_value = task
|
|
mock_repo.update.side_effect = lambda t: t
|
|
|
|
uc = RetryGenerationTaskUseCase(mock_repo)
|
|
result = uc.execute("task-1")
|
|
|
|
assert result.id == original_id
|
|
|
|
|
|
# ============================================================
|
|
# Command / Filter / Result 对象
|
|
# ============================================================
|
|
|
|
|
|
class TestCommandAndDataObjects:
|
|
"""命令对象和数据对象"""
|
|
|
|
def test_create_command_defaults(self):
|
|
cmd = CreateGenerationTaskCommand()
|
|
assert cmd.project_id == ""
|
|
assert cmd.asset_library_id == ""
|
|
assert cmd.asset_ids == []
|
|
assert cmd.title_ids == []
|
|
assert cmd.voice_ids == []
|
|
assert cmd.auto_retry_enabled is False
|
|
assert cmd.auto_retry_max == 0
|
|
|
|
def test_list_filter_defaults(self):
|
|
f = ListTasksFilter()
|
|
assert f.status is None
|
|
|
|
def test_list_result(self):
|
|
task = make_task()
|
|
r = ListGenerationTasksResult(items=[task], total=1)
|
|
assert len(r.items) == 1
|
|
assert r.total == 1
|