fix: 状态枚举添加_missing_兼容历史脏数据,修复Staging 500 #809
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m57s
AI Code Review / AI Code Review (pull_request) Successful in 3m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m40s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 16m20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m37s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 9s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1286h28m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1286h44m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1286h44m16s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1286h44m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1286h59m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1286h59m19s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1286h59m15s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1286h59m21s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1287h13m44s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1287h13m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1287h13m46s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1287h16m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1287h31m47s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m57s
AI Code Review / AI Code Review (pull_request) Successful in 3m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m40s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 16m20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m37s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 9s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1286h28m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1286h44m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1286h44m16s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1286h44m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1286h59m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1286h59m19s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1286h59m15s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1286h59m21s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1287h13m44s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1287h13m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1287h13m46s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1287h16m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1287h31m47s
根因:Staging PostgreSQL有历史脏数据(如success/done等旧状态值), GenerationTaskStatus/EditPlanStatus/EditPlanClipStatus枚举缺少_missing_ 兼容方法,导致枚举转换抛ValueError触发500。 修复:参照AssetStatus/ClassificationJobStatus的模式,给三个枚举都加上 _missing_方法,兼容常见历史别名,未知值兜底到初始态(不阻塞业务)。 修改范围: - packages/domain/generation_task.py: GenerationTaskStatus._missing_ - packages/domain/edit_plan.py: EditPlanStatus._missing_ - packages/domain/edit_plan_clip.py: EditPlanClipStatus._missing_ - tests/unit/test_generation_task_status_enum.py: 32个兼容性单测
This commit is contained in:
@@ -28,6 +28,28 @@ class EditPlanStatus(StrEnum):
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> "EditPlanStatus":
|
||||
"""兼容历史脏数据,避免枚举转换失败导致500。
|
||||
|
||||
- success/done/finished/complete → COMPLETED
|
||||
- fail/error/err → FAILED
|
||||
- render/rendering → RENDERING
|
||||
- edit/editing → EDITING
|
||||
- 其他未知值 → DRAFT(兜底,不阻塞业务)
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ("done", "success", "finished", "complete", "completed"):
|
||||
return cls.COMPLETED
|
||||
if normalized in ("fail", "failed", "error", "err"):
|
||||
return cls.FAILED
|
||||
if normalized in ("render", "rendering", "generating", "generating_video"):
|
||||
return cls.RENDERING
|
||||
if normalized in ("edit", "editing", "working"):
|
||||
return cls.EDITING
|
||||
return cls.DRAFT
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EditPlan:
|
||||
|
||||
@@ -31,6 +31,25 @@ class EditPlanClipStatus(StrEnum):
|
||||
RENDERED = "rendered" # 已渲染
|
||||
FAILED = "failed" # 渲染失败
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> "EditPlanClipStatus":
|
||||
"""兼容历史脏数据,避免枚举转换失败导致500。
|
||||
|
||||
- success/done/finished/complete/rendered → RENDERED
|
||||
- fail/error/err → FAILED
|
||||
- ready/available → READY
|
||||
- 其他未知值 → PENDING(兜底,不阻塞业务)
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ("done", "success", "finished", "complete", "rendered", "render"):
|
||||
return cls.RENDERED
|
||||
if normalized in ("fail", "failed", "error", "err"):
|
||||
return cls.FAILED
|
||||
if normalized in ("ready", "available", "prepared"):
|
||||
return cls.READY
|
||||
return cls.PENDING
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EditPlanClip:
|
||||
|
||||
@@ -43,6 +43,28 @@ class GenerationTaskStatus(StrEnum):
|
||||
CANCELLED = "cancelled"
|
||||
"""已取消(用户取消或系统取消)"""
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> "GenerationTaskStatus":
|
||||
"""兼容历史脏数据,避免枚举转换失败导致500。
|
||||
|
||||
- success/done/finished/complete → COMPLETED
|
||||
- fail/error/err → FAILED
|
||||
- process/processing/run/running → RUNNING
|
||||
- cancel/canceled → CANCELLED
|
||||
- 其他未知值 → PENDING(兜底,不阻塞业务)
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ("done", "success", "finished", "complete", "completed"):
|
||||
return cls.COMPLETED
|
||||
if normalized in ("fail", "failed", "error", "err"):
|
||||
return cls.FAILED
|
||||
if normalized in ("process", "processing", "run", "running", "in_progress"):
|
||||
return cls.RUNNING
|
||||
if normalized in ("cancel", "cancelled", "canceled"):
|
||||
return cls.CANCELLED
|
||||
return cls.PENDING
|
||||
|
||||
|
||||
# 终态集合
|
||||
TERMINAL_STATUSES = frozenset(
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
"""GenerationTaskStatus 枚举兼容性测试。
|
||||
|
||||
验证历史脏数据(如 'success'/'done')不会导致枚举转换失败。
|
||||
关联 Issue: #809 [Staging] E2E测试失败 - 模板生成接口返回500
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.generation_task import GenerationTaskStatus
|
||||
|
||||
|
||||
class TestGenerationTaskStatusNormalValues:
|
||||
"""正常值应该正确映射。"""
|
||||
|
||||
def test_pending(self):
|
||||
assert GenerationTaskStatus("pending") == GenerationTaskStatus.PENDING
|
||||
|
||||
def test_running(self):
|
||||
assert GenerationTaskStatus("running") == GenerationTaskStatus.RUNNING
|
||||
|
||||
def test_completed(self):
|
||||
assert GenerationTaskStatus("completed") == GenerationTaskStatus.COMPLETED
|
||||
|
||||
def test_failed(self):
|
||||
assert GenerationTaskStatus("failed") == GenerationTaskStatus.FAILED
|
||||
|
||||
def test_cancelled(self):
|
||||
assert GenerationTaskStatus("cancelled") == GenerationTaskStatus.CANCELLED
|
||||
|
||||
|
||||
class TestGenerationTaskStatusHistoricalValues:
|
||||
"""历史脏数据应该正确映射到对应状态,不抛异常。"""
|
||||
|
||||
@pytest.mark.parametrize("value", ["done", "success", "finished", "complete", "completed"])
|
||||
def test_completed_like_values_map_to_completed(self, value):
|
||||
assert GenerationTaskStatus(value) == GenerationTaskStatus.COMPLETED
|
||||
|
||||
@pytest.mark.parametrize("value", ["fail", "failed", "error", "err"])
|
||||
def test_failed_like_values_map_to_failed(self, value):
|
||||
assert GenerationTaskStatus(value) == GenerationTaskStatus.FAILED
|
||||
|
||||
@pytest.mark.parametrize("value", ["process", "processing", "run", "running", "in_progress"])
|
||||
def test_running_like_values_map_to_running(self, value):
|
||||
assert GenerationTaskStatus(value) == GenerationTaskStatus.RUNNING
|
||||
|
||||
@pytest.mark.parametrize("value", ["cancel", "cancelled", "canceled"])
|
||||
def test_cancelled_like_values_map_to_cancelled(self, value):
|
||||
assert GenerationTaskStatus(value) == GenerationTaskStatus.CANCELLED
|
||||
|
||||
@pytest.mark.parametrize("value", [" Done ", "SUCCESS", " failed "])
|
||||
def test_whitespace_and_case_insensitive(self, value):
|
||||
"""带空格和大小写不影响匹配。"""
|
||||
# 只要能找到对应状态且不抛异常即可
|
||||
result = GenerationTaskStatus(value)
|
||||
assert result in (
|
||||
GenerationTaskStatus.COMPLETED,
|
||||
GenerationTaskStatus.FAILED,
|
||||
)
|
||||
|
||||
|
||||
class TestGenerationTaskStatusFallback:
|
||||
"""完全未知的值兜底为 PENDING,不抛500。"""
|
||||
|
||||
@pytest.mark.parametrize("value", ["unknown", "foo_bar", "deleted", ""])
|
||||
def test_unknown_value_falls_back_to_pending(self, value):
|
||||
assert GenerationTaskStatus(value) == GenerationTaskStatus.PENDING
|
||||
|
||||
def test_none_value_falls_back_to_pending(self):
|
||||
assert GenerationTaskStatus(None) == GenerationTaskStatus.PENDING # type: ignore[arg-type]
|
||||
|
||||
def test_int_value_falls_back_to_pending(self):
|
||||
assert GenerationTaskStatus(123) == GenerationTaskStatus.PENDING # type: ignore[arg-type]
|
||||
|
||||
|
||||
class TestGenerationTaskStatusStrValue:
|
||||
"""枚举值仍为字符串类型,不影响序列化。"""
|
||||
|
||||
def test_value_unchanged(self):
|
||||
assert GenerationTaskStatus.PENDING.value == "pending"
|
||||
assert GenerationTaskStatus.COMPLETED.value == "completed"
|
||||
assert isinstance(GenerationTaskStatus.PENDING, str)
|
||||
Reference in New Issue
Block a user