1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. 未使用依赖清理:
- 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL
2. pyflakes 警告清零 (apps/ + packages/ + tests/):
- 移除 17 处未使用的 import (F401)
- 修复 26 处未使用的局部变量 (F841):
* 有副作用的赋值转为裸调用
* 无副作用的赋值直接删除
- 修复 1 处未使用的异常变量 (F841)
- 修复 1 处空 except 块
3. 测试文件冗余清理:
- 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
- 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
585 lines
22 KiB
Python
Executable File
585 lines
22 KiB
Python
Executable File
"""Unit tests for JobService — Phase 8 任务 2.10.
|
|
|
|
测试 Job 领域模型、Use Cases 和 Service 层。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
|
|
|
from packages.application.jobs import (
|
|
CancelJobUseCase,
|
|
CompleteJobCommand,
|
|
CompleteJobUseCase,
|
|
CreateJobCommand,
|
|
CreateJobUseCase,
|
|
FailJobCommand,
|
|
FailJobUseCase,
|
|
GetJobStatisticsUseCase,
|
|
GetJobUseCase,
|
|
ListJobsUseCase,
|
|
RetryJobUseCase,
|
|
SubmitJobUseCase,
|
|
UpdateJobProgressCommand,
|
|
UpdateJobProgressUseCase,
|
|
)
|
|
from packages.domain.job import Job, JobStatus, JobType
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class FakeJobRepo:
|
|
"""内存中的 JobRepository 测试替身。"""
|
|
|
|
def __init__(self):
|
|
self._store: dict[str, Job] = {}
|
|
|
|
def create(self, job: Job) -> Job:
|
|
self._store[job.id] = job
|
|
return job
|
|
|
|
def get(self, job_id: str) -> Job | None:
|
|
return self._store.get(job_id)
|
|
|
|
def update(self, job: Job) -> Job:
|
|
if job.id not in self._store:
|
|
raise ValueError(f"Job {job.id} not found")
|
|
self._store[job.id] = job
|
|
return job
|
|
|
|
def list_by_project(self, project_id, *, job_type=None, status=None, limit=50, offset=0):
|
|
results = [
|
|
j
|
|
for j in self._store.values()
|
|
if j.project_id == project_id
|
|
and (job_type is None or j.job_type == job_type or j.job_type == JobType(job_type))
|
|
and (status is None or j.status == status or j.status == JobStatus(status))
|
|
]
|
|
return results[offset : offset + limit]
|
|
|
|
def list_by_user(self, user_id, *, job_type=None, status=None, limit=50, offset=0):
|
|
results = [
|
|
j
|
|
for j in self._store.values()
|
|
if j.created_by_user_id == user_id
|
|
and (job_type is None or j.job_type == job_type or j.job_type == JobType(job_type))
|
|
and (status is None or j.status == status or j.status == JobStatus(status))
|
|
]
|
|
return results[offset : offset + limit]
|
|
|
|
def count_by_project(self, project_id, *, status=None):
|
|
return len(
|
|
[
|
|
j
|
|
for j in self._store.values()
|
|
if j.project_id == project_id
|
|
and (status is None or j.status == status or j.status == JobStatus(status))
|
|
]
|
|
)
|
|
|
|
def find_active_by_source(self, source_id, job_type):
|
|
jt = job_type.value if isinstance(job_type, JobType) else job_type
|
|
for j in self._store.values():
|
|
if (
|
|
j.source_id == source_id
|
|
and j.job_type.value == jt
|
|
and j.status in (JobStatus.PENDING, JobStatus.RUNNING)
|
|
):
|
|
return j
|
|
return None
|
|
|
|
|
|
@pytest.fixture
|
|
def repo():
|
|
return FakeJobRepo()
|
|
|
|
|
|
# ── Job 领域模型测试 ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestJobDomainModel:
|
|
def test_create_job(self):
|
|
job = Job.create(
|
|
project_id="proj-1",
|
|
job_type=JobType.VIDEO_COMPOSE,
|
|
payload={"plan_id": "plan-1"},
|
|
source_id="plan-1",
|
|
created_by_user_id="user-1",
|
|
)
|
|
assert job.project_id == "proj-1"
|
|
assert job.job_type == JobType.VIDEO_COMPOSE
|
|
assert job.status == JobStatus.PENDING
|
|
assert job.progress == 0.0
|
|
assert job.retry_count == 0
|
|
assert job.max_retries == 3
|
|
assert job.payload == {"plan_id": "plan-1"}
|
|
|
|
def test_create_job_with_string_type(self):
|
|
job = Job.create(project_id="proj-1", job_type="render_edit_plan")
|
|
assert job.job_type == JobType.RENDER_EDIT_PLAN
|
|
|
|
def test_create_job_invalid_type_raises(self):
|
|
with pytest.raises(ValueError, match="不支持的任务类型"):
|
|
Job.create(project_id="proj-1", job_type="invalid_type")
|
|
|
|
def test_create_job_empty_project_id_raises(self):
|
|
with pytest.raises(ValueError, match="project_id 不能为空"):
|
|
Job.create(project_id=" ", job_type=JobType.VIDEO_COMPOSE)
|
|
|
|
def test_is_terminal(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
assert not job.is_terminal
|
|
|
|
job.status = JobStatus.SUCCESS
|
|
assert job.is_terminal
|
|
|
|
job.status = JobStatus.FAILED
|
|
assert job.is_terminal
|
|
|
|
job.status = JobStatus.CANCELLED
|
|
assert job.is_terminal
|
|
|
|
def test_is_retryable(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, max_retries=2)
|
|
job.status = JobStatus.FAILED
|
|
assert job.is_retryable
|
|
|
|
job.retry_count = 2
|
|
assert not job.is_retryable
|
|
|
|
def test_mark_running(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.mark_running("初始化中")
|
|
assert job.status == JobStatus.RUNNING
|
|
assert job.current_stage == "初始化中"
|
|
assert job.started_at is not None
|
|
|
|
def test_mark_success(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.mark_running()
|
|
job.mark_success(result={"output_url": "https://example.com/video.mp4"})
|
|
assert job.status == JobStatus.SUCCESS
|
|
assert job.progress == 100.0
|
|
assert job.current_stage == "完成"
|
|
assert job.result == {"output_url": "https://example.com/video.mp4"}
|
|
assert job.completed_at is not None
|
|
|
|
def test_mark_failed(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.mark_running()
|
|
job.mark_failed("FFmpeg 执行失败")
|
|
assert job.status == JobStatus.FAILED
|
|
assert job.error_message == "FFmpeg 执行失败"
|
|
assert job.completed_at is not None
|
|
|
|
def test_mark_cancelled(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.mark_cancelled()
|
|
assert job.status == JobStatus.CANCELLED
|
|
assert job.current_stage == "已取消"
|
|
|
|
def test_update_progress(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.update_progress(50.0, "合成中")
|
|
assert job.progress == 50.0
|
|
assert job.current_stage == "合成中"
|
|
|
|
def test_update_progress_invalid_value_raises(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
with pytest.raises(ValueError, match="进度必须在 0~100 之间"):
|
|
job.update_progress(150.0)
|
|
|
|
def test_prepare_retry(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, max_retries=3)
|
|
job.mark_running()
|
|
job.mark_failed("临时错误")
|
|
job.prepare_retry()
|
|
assert job.status == JobStatus.PENDING
|
|
assert job.retry_count == 1
|
|
assert job.progress == 0.0
|
|
assert job.error_message == ""
|
|
assert "第 1 次重试" in job.current_stage
|
|
|
|
def test_prepare_retry_exceeded_raises(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, max_retries=1)
|
|
job.mark_running()
|
|
job.mark_failed("错误")
|
|
job.prepare_retry() # retry_count = 1, max_retries = 1 → not retryable
|
|
with pytest.raises(ValueError, match="任务不可重试"):
|
|
job.prepare_retry()
|
|
|
|
def test_invalid_transition_raises(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
job.mark_running()
|
|
job.mark_success()
|
|
with pytest.raises(ValueError, match="非法状态转换"):
|
|
job.transition_to(JobStatus.RUNNING) # success → running 不合法(终态不可转换)
|
|
|
|
def test_to_dict(self):
|
|
job = Job.create(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
d = job.to_dict()
|
|
assert d["project_id"] == "proj-1"
|
|
assert d["job_type"] == "video_compose"
|
|
assert d["status"] == "pending"
|
|
assert "is_retryable" in d
|
|
|
|
|
|
# ── Use Case 测试 ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestCreateJobUseCase:
|
|
def test_execute(self, repo):
|
|
use_case = CreateJobUseCase(repo)
|
|
cmd = CreateJobCommand(
|
|
project_id="proj-1",
|
|
job_type=JobType.VIDEO_COMPOSE,
|
|
payload={"plan_id": "plan-1"},
|
|
source_id="plan-1",
|
|
created_by_user_id="user-1",
|
|
)
|
|
job = use_case.execute(cmd)
|
|
assert job.id in repo._store
|
|
assert job.status == JobStatus.PENDING
|
|
|
|
def test_execute_with_string_type(self, repo):
|
|
use_case = CreateJobUseCase(repo)
|
|
cmd = CreateJobCommand(
|
|
project_id="proj-1",
|
|
job_type="video_compose",
|
|
)
|
|
job = use_case.execute(cmd)
|
|
assert job.job_type == JobType.VIDEO_COMPOSE
|
|
|
|
|
|
class TestSubmitJobUseCase:
|
|
def test_submit_pending_job(self, repo):
|
|
# 先创建
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
# 提交
|
|
submit_uc = SubmitJobUseCase(repo)
|
|
submitted = submit_uc.execute(job.id, celery_task_id="celery-abc")
|
|
assert submitted.status == JobStatus.RUNNING
|
|
assert submitted.celery_task_id == "celery-abc"
|
|
assert submitted.started_at is not None
|
|
|
|
def test_submit_non_pending_raises(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
repo.update(job)
|
|
|
|
submit_uc = SubmitJobUseCase(repo)
|
|
with pytest.raises(ValueError, match="只有 pending 状态"):
|
|
submit_uc.execute(job.id)
|
|
|
|
def test_submit_nonexistent_raises(self, repo):
|
|
submit_uc = SubmitJobUseCase(repo)
|
|
with pytest.raises(ValueError, match="任务不存在"):
|
|
submit_uc.execute("nonexistent-id")
|
|
|
|
|
|
class TestUpdateJobProgressUseCase:
|
|
def test_update_running_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
repo.update(job)
|
|
|
|
progress_uc = UpdateJobProgressUseCase(repo)
|
|
updated = progress_uc.execute(UpdateJobProgressCommand(job_id=job.id, progress=75.0, current_stage="渲染中"))
|
|
assert updated.progress == 75.0
|
|
assert updated.current_stage == "渲染中"
|
|
|
|
def test_update_non_running_raises(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
progress_uc = UpdateJobProgressUseCase(repo)
|
|
with pytest.raises(ValueError, match="只有 running 状态"):
|
|
progress_uc.execute(UpdateJobProgressCommand(job_id=job.id, progress=50.0))
|
|
|
|
|
|
class TestCompleteJobUseCase:
|
|
def test_complete_running_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
repo.update(job)
|
|
|
|
complete_uc = CompleteJobUseCase(repo)
|
|
completed = complete_uc.execute(CompleteJobCommand(job_id=job.id, result={"url": "https://example.com/v.mp4"}))
|
|
assert completed.status == JobStatus.SUCCESS
|
|
assert completed.progress == 100.0
|
|
assert completed.result == {"url": "https://example.com/v.mp4"}
|
|
|
|
def test_complete_non_running_raises(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
job.mark_success() # 已经是终态
|
|
|
|
complete_uc = CompleteJobUseCase(repo)
|
|
with pytest.raises(ValueError, match="只有 running/pending 状态"):
|
|
complete_uc.execute(CompleteJobCommand(job_id=job.id))
|
|
|
|
|
|
class TestFailJobUseCase:
|
|
def test_fail_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
repo.update(job)
|
|
|
|
fail_uc = FailJobUseCase(repo)
|
|
failed = fail_uc.execute(FailJobCommand(job_id=job.id, error_message="磁盘空间不足"))
|
|
assert failed.status == JobStatus.FAILED
|
|
assert failed.error_message == "磁盘空间不足"
|
|
|
|
|
|
class TestRetryJobUseCase:
|
|
def test_retry_failed_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, max_retries=3))
|
|
job.mark_running()
|
|
job.mark_failed("网络超时")
|
|
repo.update(job)
|
|
|
|
retry_uc = RetryJobUseCase(repo)
|
|
retried = retry_uc.execute(job.id)
|
|
assert retried.status == JobStatus.PENDING
|
|
assert retried.retry_count == 1
|
|
assert retried.error_message == ""
|
|
|
|
def test_retry_non_failed_raises(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
retry_uc = RetryJobUseCase(repo)
|
|
with pytest.raises(ValueError, match="任务不可重试"):
|
|
retry_uc.execute(job.id)
|
|
|
|
|
|
class TestCancelJobUseCase:
|
|
def test_cancel_pending_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
cancel_uc = CancelJobUseCase(repo)
|
|
cancelled = cancel_uc.execute(job.id)
|
|
assert cancelled.status == JobStatus.CANCELLED
|
|
|
|
def test_cancel_completed_raises(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
job.mark_running()
|
|
job.mark_success()
|
|
repo.update(job)
|
|
|
|
cancel_uc = CancelJobUseCase(repo)
|
|
with pytest.raises(ValueError, match="已处于终态"):
|
|
cancel_uc.execute(job.id)
|
|
|
|
|
|
class TestGetJobUseCase:
|
|
def test_get_existing_job(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
job = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
get_uc = GetJobUseCase(repo)
|
|
fetched = get_uc.execute(job.id)
|
|
assert fetched is not None
|
|
assert fetched.id == job.id
|
|
|
|
def test_get_nonexistent_returns_none(self, repo):
|
|
get_uc = GetJobUseCase(repo)
|
|
assert get_uc.execute("nonexistent") is None
|
|
|
|
|
|
class TestListJobsUseCase:
|
|
def test_list_by_project(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.RENDER_EDIT_PLAN))
|
|
create_uc.execute(CreateJobCommand(project_id="proj-2", job_type=JobType.VIDEO_COMPOSE))
|
|
|
|
list_uc = ListJobsUseCase(repo)
|
|
jobs = list_uc.execute(project_id="proj-1")
|
|
assert len(jobs) == 2
|
|
|
|
def test_list_by_project_with_type_filter(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.RENDER_EDIT_PLAN))
|
|
|
|
list_uc = ListJobsUseCase(repo)
|
|
jobs = list_uc.execute(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE)
|
|
assert len(jobs) == 1
|
|
assert jobs[0].job_type == JobType.VIDEO_COMPOSE
|
|
|
|
def test_list_by_project_with_status_filter(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j2 = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j2.mark_running()
|
|
repo.update(j2)
|
|
|
|
list_uc = ListJobsUseCase(repo)
|
|
running_jobs = list_uc.execute(project_id="proj-1", status=JobStatus.RUNNING)
|
|
assert len(running_jobs) == 1
|
|
|
|
def test_list_by_user(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
create_uc.execute(
|
|
CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, created_by_user_id="user-1")
|
|
)
|
|
create_uc.execute(
|
|
CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE, created_by_user_id="user-2")
|
|
)
|
|
|
|
list_uc = ListJobsUseCase(repo)
|
|
jobs = list_uc.execute(user_id="user-1")
|
|
assert len(jobs) == 1
|
|
|
|
def test_list_without_project_or_user_raises(self, repo):
|
|
list_uc = ListJobsUseCase(repo)
|
|
with pytest.raises(ValueError, match="必须指定"):
|
|
list_uc.execute()
|
|
|
|
|
|
class TestGetJobStatisticsUseCase:
|
|
def test_statistics(self, repo):
|
|
create_uc = CreateJobUseCase(repo)
|
|
create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j2 = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j2.mark_running()
|
|
repo.update(j2)
|
|
j3 = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j3.mark_running()
|
|
j3.mark_success()
|
|
repo.update(j3)
|
|
j4 = create_uc.execute(CreateJobCommand(project_id="proj-1", job_type=JobType.VIDEO_COMPOSE))
|
|
j4.mark_running()
|
|
j4.mark_failed("err")
|
|
repo.update(j4)
|
|
|
|
stats_uc = GetJobStatisticsUseCase(repo)
|
|
stats = stats_uc.execute("proj-1")
|
|
assert stats["total"] == 4
|
|
assert stats["pending"] == 1
|
|
assert stats["running"] == 1
|
|
assert stats["success"] == 1
|
|
assert stats["failed"] == 1
|
|
|
|
|
|
# ── JobService 集成测试 ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestJobService:
|
|
def test_create_compose_job(self, repo):
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
job = svc.create_compose_job(
|
|
project_id="proj-1",
|
|
plan_id="plan-1",
|
|
user_id="user-1",
|
|
)
|
|
assert job.job_type == JobType.VIDEO_COMPOSE
|
|
assert job.payload == {"plan_id": "plan-1"}
|
|
assert job.source_id == "plan-1"
|
|
assert job.created_by_user_id == "user-1"
|
|
|
|
def test_create_render_job(self, repo):
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
job = svc.create_render_job(
|
|
project_id="proj-1",
|
|
plan_id="plan-1",
|
|
user_id="user-1",
|
|
)
|
|
assert job.job_type == JobType.RENDER_EDIT_PLAN
|
|
|
|
def test_submit_compose_if_not_exists_creates_new(self, repo):
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
job, created = svc.submit_compose_if_not_exists("proj-1", "plan-1", "user-1", "celery-1")
|
|
assert created is True
|
|
assert job.status == JobStatus.RUNNING
|
|
assert job.celery_task_id == "celery-1"
|
|
|
|
def test_submit_compose_if_not_exists_returns_existing(self, repo):
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
job1, created1 = svc.submit_compose_if_not_exists("proj-1", "plan-1", "user-1", "celery-1")
|
|
job2, created2 = svc.submit_compose_if_not_exists("proj-1", "plan-1", "user-1", "celery-2")
|
|
assert created2 is False
|
|
assert job2.id == job1.id
|
|
|
|
def test_full_lifecycle(self, repo):
|
|
"""完整生命周期测试:创建 → 提交 → 更新进度 → 完成。"""
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
|
|
# 创建
|
|
job = svc.create_compose_job("proj-1", "plan-1", "user-1")
|
|
assert job.status == JobStatus.PENDING
|
|
|
|
# 提交
|
|
job = svc.submit_job(job.id, "celery-xyz")
|
|
assert job.status == JobStatus.RUNNING
|
|
assert job.celery_task_id == "celery-xyz"
|
|
|
|
# 更新进度
|
|
job = svc.update_progress(job.id, 50.0, "合成中")
|
|
assert job.progress == 50.0
|
|
|
|
job = svc.update_progress(job.id, 80.0, "上传结果")
|
|
assert job.progress == 80.0
|
|
|
|
# 完成
|
|
job = svc.complete_job(job.id, result={"url": "https://example.com/v.mp4"})
|
|
assert job.status == JobStatus.SUCCESS
|
|
assert job.progress == 100.0
|
|
|
|
def test_full_lifecycle_with_retry(self, repo):
|
|
"""带重试的完整生命周期测试。"""
|
|
from app.services.job_service import JobService
|
|
|
|
svc = JobService(repo)
|
|
|
|
# 创建并执行
|
|
job = svc.create_compose_job("proj-1", "plan-1", "user-1", max_retries=2)
|
|
job = svc.submit_job(job.id, "celery-1")
|
|
|
|
# 失败
|
|
job = svc.fail_job(job.id, "临时网络错误")
|
|
assert job.status == JobStatus.FAILED
|
|
|
|
# 重试
|
|
job = svc.retry_job(job.id)
|
|
assert job.status == JobStatus.PENDING
|
|
assert job.retry_count == 1
|
|
|
|
# 再次提交
|
|
job = svc.submit_job(job.id, "celery-2")
|
|
assert job.status == JobStatus.RUNNING
|
|
|
|
# 成功
|
|
job = svc.complete_job(job.id, result={"url": "ok"})
|
|
assert job.status == JobStatus.SUCCESS
|