From 528bcaf59fb1a5709bdacf159b5b5ad45cc59ad4 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Fri, 24 Jul 2026 18:01:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=8A=B6=E6=80=81=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=5Fmissing=5F=E5=85=BC=E5=AE=B9=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=84=8F=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?Staging=20500=20#809?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: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个兼容性单测 --- packages/domain/edit_plan.py | 22 +++++ packages/domain/edit_plan_clip.py | 19 +++++ packages/domain/generation_task.py | 22 +++++ .../unit/test_generation_task_status_enum.py | 81 +++++++++++++++++++ 4 files changed, 144 insertions(+) create mode 100755 tests/unit/test_generation_task_status_enum.py diff --git a/packages/domain/edit_plan.py b/packages/domain/edit_plan.py index 4eec3e915..0f8175190 100755 --- a/packages/domain/edit_plan.py +++ b/packages/domain/edit_plan.py @@ -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: diff --git a/packages/domain/edit_plan_clip.py b/packages/domain/edit_plan_clip.py index 748f2567a..b5da75911 100755 --- a/packages/domain/edit_plan_clip.py +++ b/packages/domain/edit_plan_clip.py @@ -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: diff --git a/packages/domain/generation_task.py b/packages/domain/generation_task.py index 76c148f7e..8c14210b1 100755 --- a/packages/domain/generation_task.py +++ b/packages/domain/generation_task.py @@ -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( diff --git a/tests/unit/test_generation_task_status_enum.py b/tests/unit/test_generation_task_status_enum.py new file mode 100755 index 000000000..dd55e0929 --- /dev/null +++ b/tests/unit/test_generation_task_status_enum.py @@ -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) -- 2.54.0