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

根因: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:
CI Bot
2026-07-24 18:01:13 +08:00
parent 464f6ea155
commit 528bcaf59f
4 changed files with 144 additions and 0 deletions
+22
View File
@@ -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:
+19
View File
@@ -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:
+22
View File
@@ -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
View File
@@ -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)