fix: 状态枚举添加_missing_兼容历史脏数据,修复Staging模板生成接口500 #809 #835

Merged
xiaoxia merged 1 commits from bugfix/809-generation-task-status-missing into develop 2026-07-25 10:04:00 +08:00
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)