From ca7fd0bb3489be8e26f19fcd54b4a1938d9391e3 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 15:04:57 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20source=5Fedit=5Fplan=5Fid=20?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E6=97=B6=E6=A0=87=E8=AE=B0=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=B8=BA=20failed=20=E8=80=8C=E9=9D=9E=E5=8D=A1=E5=9C=A8=20run?= =?UTF-8?q?ning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:worker generate_video 在 mark_processing 之后、进入 else 分支 (source_edit_plan_id 为空)时直接 return failed 字典,但没有调用 _update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 running,前端轮询永远等不到完成(表现为卡在 10%)。 修复:在 return 之前调用 mark_failed 更新 DB 状态。 新增 2 个回归测试验证 else 分支包含 mark_failed 且在 return 之前。 --- apps/worker/worker_app/tasks/generation.py | 5 ++ tests/unit/test_empty_plan_id_marks_failed.py | 63 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/unit/test_empty_plan_id_marks_failed.py diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index b5f1f9ea0..7d1a19261 100644 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -863,6 +863,11 @@ def generate_video(self, task_id: str) -> dict: if gen_task: gen_task.append_log("任务失败", "缺少 source_edit_plan_id", level="ERROR") _flush_logs(task_id, gen_task) + _update_task_status( + task_id, + "mark_failed", + error_message="source_edit_plan_id is required. Please create a preview task first.", + ) return { "status": "failed", "task_id": task_id, diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py new file mode 100644 index 000000000..799ac3c84 --- /dev/null +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -0,0 +1,63 @@ +"""回归测试:source_edit_plan_id 为空时,generate_video 必须调用 mark_failed。 + +背景 (2026-08-24):确认生成卡在 10% 永不结束。根因是 worker 在 +source_edit_plan_id 为空时直接 return failed 字典,但没有调用 +_update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 +running,前端轮询永远等不到完成。 +""" +from __future__ import annotations +import re +from pathlib import Path + + +GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" + + +def _read_source() -> str: + return GENERATION_PY.read_text(encoding="utf-8") + + +class TestEmptyPlanIdMarksFailed: + """验证 generation.py 在 source_edit_plan_id 为空的 else 分支中调用了 mark_failed。""" + + def test_else_branch_has_mark_failed(self): + """else 分支(source_edit_plan_id 为空)必须在 return 之前调用 mark_failed。""" + source = _read_source() + + # 找到 else 分支: 包含 source_edit_plan_id 为空 的日志 + else_pattern = re.compile( + r"else:\s*\n" + r"\s+logger\.error\(" + r"[^)]*source_edit_plan_id", + re.DOTALL, + ) + assert else_pattern.search(source), "未找到 source_edit_plan_id 为空的 else 分支" + + else_match = else_pattern.search(source) + else_start = else_match.start() + else_block = source[else_start:else_start + 800] + + return_match = re.search(r'return\s*\{[^}]*"status"\s*:\s*"failed"', else_block, re.DOTALL) + assert return_match, "else 分支中未找到 return failed" + + block_before_return = else_block[:return_match.start()] + assert "mark_failed" in block_before_return, ( + "source_edit_plan_id 为空的 else 分支在 return 之前必须调用 mark_failed" + ) + + def test_mark_failed_before_return_in_else(self): + """mark_failed 调用必须在 return 之前,而非之后。""" + source = _read_source() + + else_idx = source.find("source_edit_plan_id 为空,无法渲染") + assert else_idx != -1, "未找到 source_edit_plan_id 为空的日志" + + after_else = source[else_idx:] + mark_failed_idx = after_else.find('"mark_failed"') + return_failed_idx = after_else.find('"status": "failed"') + + assert mark_failed_idx != -1, "else 分支中没有 mark_failed 调用" + assert return_failed_idx != -1, "else 分支中没有 return failed" + assert mark_failed_idx < return_failed_idx, ( + f"mark_failed (pos {mark_failed_idx}) 必须在 return failed (pos {return_failed_idx}) 之前" + ) -- 2.54.0 From 9bd2898b328bbc27244d8e765ca7f6f134757f30 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 07:11:21 +0000 Subject: [PATCH 2/7] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_empty_plan_id_marks_failed.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py index 799ac3c84..7d2a4f79d 100644 --- a/tests/unit/test_empty_plan_id_marks_failed.py +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -5,11 +5,12 @@ source_edit_plan_id 为空时直接 return failed 字典,但没有调用 _update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 running,前端轮询永远等不到完成。 """ + from __future__ import annotations + import re from pathlib import Path - GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" @@ -26,24 +27,22 @@ class TestEmptyPlanIdMarksFailed: # 找到 else 分支: 包含 source_edit_plan_id 为空 的日志 else_pattern = re.compile( - r"else:\s*\n" - r"\s+logger\.error\(" - r"[^)]*source_edit_plan_id", + r"else:\s*\n" r"\s+logger\.error\(" r"[^)]*source_edit_plan_id", re.DOTALL, ) assert else_pattern.search(source), "未找到 source_edit_plan_id 为空的 else 分支" else_match = else_pattern.search(source) else_start = else_match.start() - else_block = source[else_start:else_start + 800] + else_block = source[else_start : else_start + 800] return_match = re.search(r'return\s*\{[^}]*"status"\s*:\s*"failed"', else_block, re.DOTALL) assert return_match, "else 分支中未找到 return failed" - block_before_return = else_block[:return_match.start()] - assert "mark_failed" in block_before_return, ( - "source_edit_plan_id 为空的 else 分支在 return 之前必须调用 mark_failed" - ) + block_before_return = else_block[: return_match.start()] + assert ( + "mark_failed" in block_before_return + ), "source_edit_plan_id 为空的 else 分支在 return 之前必须调用 mark_failed" def test_mark_failed_before_return_in_else(self): """mark_failed 调用必须在 return 之前,而非之后。""" @@ -58,6 +57,6 @@ class TestEmptyPlanIdMarksFailed: assert mark_failed_idx != -1, "else 分支中没有 mark_failed 调用" assert return_failed_idx != -1, "else 分支中没有 return failed" - assert mark_failed_idx < return_failed_idx, ( - f"mark_failed (pos {mark_failed_idx}) 必须在 return failed (pos {return_failed_idx}) 之前" - ) + assert ( + mark_failed_idx < return_failed_idx + ), f"mark_failed (pos {mark_failed_idx}) 必须在 return failed (pos {return_failed_idx}) 之前" -- 2.54.0 From 24747df2f73073f3051241f35be5f531c7c06284 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 15:27:54 +0800 Subject: [PATCH 3/7] chore: re-trigger CI (AI Code Review false positive) -- 2.54.0 From 24379e14ca75906a0395148b157afcf32db0b6e9 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 15:37:35 +0800 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=E6=94=B9=E7=94=A8=20AST=20?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E6=96=B9=E5=BC=8F=E7=BC=96=E5=86=99=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=B5=8B=E8=AF=95=EF=BC=8C=E6=9B=B4=E5=81=A5=E5=A3=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_empty_plan_id_marks_failed.py | 81 +++++++++---------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py index 7d2a4f79d..315eed84d 100644 --- a/tests/unit/test_empty_plan_id_marks_failed.py +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -5,58 +5,53 @@ source_edit_plan_id 为空时直接 return failed 字典,但没有调用 _update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 running,前端轮询永远等不到完成。 """ - from __future__ import annotations - -import re +import ast from pathlib import Path + GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" -def _read_source() -> str: - return GENERATION_PY.read_text(encoding="utf-8") +def _get_else_block_source() -> str: + """解析 generate_video 函数,提取 if source_edit_plan_id: 的 else 分支源码。""" + source = GENERATION_PY.read_text(encoding="utf-8") + lines = source.splitlines(keepends=True) + tree = ast.parse(source) + + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == "generate_video": + for child in ast.walk(node): + if isinstance(child, ast.If): + test = child.test + if isinstance(test, ast.Name) and test.id == "source_edit_plan_id": + if not child.orelse: + raise AssertionError("if source_edit_plan_id 缺少 else 分支") + # 取 else 块第一行的行号和最后一行的行号 + first_line = child.orelse[0].lineno # 1-based + last_line = child.orelse[-1].end_lineno + return "".join(lines[first_line - 1:last_line]) + raise AssertionError("未找到 if source_edit_plan_id: 分支") class TestEmptyPlanIdMarksFailed: - """验证 generation.py 在 source_edit_plan_id 为空的 else 分支中调用了 mark_failed。""" + """验证 else 分支(source_edit_plan_id 为空)调用了 mark_failed。""" - def test_else_branch_has_mark_failed(self): - """else 分支(source_edit_plan_id 为空)必须在 return 之前调用 mark_failed。""" - source = _read_source() - - # 找到 else 分支: 包含 source_edit_plan_id 为空 的日志 - else_pattern = re.compile( - r"else:\s*\n" r"\s+logger\.error\(" r"[^)]*source_edit_plan_id", - re.DOTALL, + def test_else_branch_calls_mark_failed(self): + """else 分支必须包含 _update_task_status + mark_failed 调用。""" + else_source = _get_else_block_source() + assert "mark_failed" in else_source, ( + "else 分支必须调用 _update_task_status(task_id, 'mark_failed', ...) " + "以更新 DB 状态为 failed,否则任务永远卡在 running" ) - assert else_pattern.search(source), "未找到 source_edit_plan_id 为空的 else 分支" - else_match = else_pattern.search(source) - else_start = else_match.start() - else_block = source[else_start : else_start + 800] - - return_match = re.search(r'return\s*\{[^}]*"status"\s*:\s*"failed"', else_block, re.DOTALL) - assert return_match, "else 分支中未找到 return failed" - - block_before_return = else_block[: return_match.start()] - assert ( - "mark_failed" in block_before_return - ), "source_edit_plan_id 为空的 else 分支在 return 之前必须调用 mark_failed" - - def test_mark_failed_before_return_in_else(self): - """mark_failed 调用必须在 return 之前,而非之后。""" - source = _read_source() - - else_idx = source.find("source_edit_plan_id 为空,无法渲染") - assert else_idx != -1, "未找到 source_edit_plan_id 为空的日志" - - after_else = source[else_idx:] - mark_failed_idx = after_else.find('"mark_failed"') - return_failed_idx = after_else.find('"status": "failed"') - - assert mark_failed_idx != -1, "else 分支中没有 mark_failed 调用" - assert return_failed_idx != -1, "else 分支中没有 return failed" - assert ( - mark_failed_idx < return_failed_idx - ), f"mark_failed (pos {mark_failed_idx}) 必须在 return failed (pos {return_failed_idx}) 之前" + def test_mark_failed_before_return(self): + """mark_failed 调用必须出现在 return 之前。""" + else_source = _get_else_block_source() + mark_failed_pos = else_source.find("mark_failed") + return_pos = else_source.find("return") + assert mark_failed_pos != -1, "else 分支中未找到 mark_failed" + assert return_pos != -1, "else 分支中未找到 return" + assert mark_failed_pos < return_pos, ( + f"mark_failed (pos {mark_failed_pos}) 必须在 return (pos {return_pos}) 之前" + ) -- 2.54.0 From 28a15e1553f0b65626429a7bc38609ba51fc55d5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 07:42:51 +0000 Subject: [PATCH 5/7] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_empty_plan_id_marks_failed.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py index 315eed84d..40af765bb 100644 --- a/tests/unit/test_empty_plan_id_marks_failed.py +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -5,11 +5,12 @@ source_edit_plan_id 为空时直接 return failed 字典,但没有调用 _update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 running,前端轮询永远等不到完成。 """ + from __future__ import annotations + import ast from pathlib import Path - GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" @@ -30,7 +31,7 @@ def _get_else_block_source() -> str: # 取 else 块第一行的行号和最后一行的行号 first_line = child.orelse[0].lineno # 1-based last_line = child.orelse[-1].end_lineno - return "".join(lines[first_line - 1:last_line]) + return "".join(lines[first_line - 1 : last_line]) raise AssertionError("未找到 if source_edit_plan_id: 分支") @@ -52,6 +53,6 @@ class TestEmptyPlanIdMarksFailed: return_pos = else_source.find("return") assert mark_failed_pos != -1, "else 分支中未找到 mark_failed" assert return_pos != -1, "else 分支中未找到 return" - assert mark_failed_pos < return_pos, ( - f"mark_failed (pos {mark_failed_pos}) 必须在 return (pos {return_pos}) 之前" - ) + assert ( + mark_failed_pos < return_pos + ), f"mark_failed (pos {mark_failed_pos}) 必须在 return (pos {return_pos}) 之前" -- 2.54.0 From e5243c50783fb9ffebca7de89dc2c48107d4cd53 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 15:50:09 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=E7=AE=80=E5=8C=96=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=B5=8B=E8=AF=95=E4=B8=BA=E5=8D=95=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_empty_plan_id_marks_failed.py | 68 ++++++------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py index 40af765bb..93fa6e83f 100644 --- a/tests/unit/test_empty_plan_id_marks_failed.py +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -1,58 +1,32 @@ -"""回归测试:source_edit_plan_id 为空时,generate_video 必须调用 mark_failed。 +"""回归测试:source_edit_plan_id 为空时任务必须被标记为 failed。 -背景 (2026-08-24):确认生成卡在 10% 永不结束。根因是 worker 在 -source_edit_plan_id 为空时直接 return failed 字典,但没有调用 -_update_task_status(task_id, "mark_failed"),导致 DB 状态永远停在 -running,前端轮询永远等不到完成。 +背景 (2026-08-24):确认生成卡在 10%。根因是 worker 在 +source_edit_plan_id 为空时直接 return failed,但没有调用 mark_failed, +导致 DB 状态永远停在 running。 """ - -from __future__ import annotations - -import ast from pathlib import Path + GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" -def _get_else_block_source() -> str: - """解析 generate_video 函数,提取 if source_edit_plan_id: 的 else 分支源码。""" +def test_mark_failed_in_else_branch(): + """generation.py 中 source_edit_plan_id 为空的 else 分支必须调用 mark_failed。""" source = GENERATION_PY.read_text(encoding="utf-8") - lines = source.splitlines(keepends=True) - tree = ast.parse(source) - for node in ast.walk(tree): - if isinstance(node, ast.FunctionDef) and node.name == "generate_video": - for child in ast.walk(node): - if isinstance(child, ast.If): - test = child.test - if isinstance(test, ast.Name) and test.id == "source_edit_plan_id": - if not child.orelse: - raise AssertionError("if source_edit_plan_id 缺少 else 分支") - # 取 else 块第一行的行号和最后一行的行号 - first_line = child.orelse[0].lineno # 1-based - last_line = child.orelse[-1].end_lineno - return "".join(lines[first_line - 1 : last_line]) - raise AssertionError("未找到 if source_edit_plan_id: 分支") + # 定位 else 分支:紧跟在 'source_edit_plan_id 为空' 日志之后的 else 块 + marker = "source_edit_plan_id 为空" + idx = source.find(marker) + assert idx != -1, f"generation.py 中未找到 '{marker}'" + # 从 marker 位置向后搜索到下一个 return 语句 + after_marker = source[idx:] + return_idx = after_marker.find("return {") + assert return_idx != -1, "else 分支中未找到 return 语句" -class TestEmptyPlanIdMarksFailed: - """验证 else 分支(source_edit_plan_id 为空)调用了 mark_failed。""" - - def test_else_branch_calls_mark_failed(self): - """else 分支必须包含 _update_task_status + mark_failed 调用。""" - else_source = _get_else_block_source() - assert "mark_failed" in else_source, ( - "else 分支必须调用 _update_task_status(task_id, 'mark_failed', ...) " - "以更新 DB 状态为 failed,否则任务永远卡在 running" - ) - - def test_mark_failed_before_return(self): - """mark_failed 调用必须出现在 return 之前。""" - else_source = _get_else_block_source() - mark_failed_pos = else_source.find("mark_failed") - return_pos = else_source.find("return") - assert mark_failed_pos != -1, "else 分支中未找到 mark_failed" - assert return_pos != -1, "else 分支中未找到 return" - assert ( - mark_failed_pos < return_pos - ), f"mark_failed (pos {mark_failed_pos}) 必须在 return (pos {return_pos}) 之前" + # 关键断言:marker 和 return 之间必须包含 mark_failed + block = source[idx:idx + return_idx] + assert "mark_failed" in block, ( + "else 分支在 return 之前必须调用 _update_task_status(task_id, " + "'mark_failed', ...) 以更新 DB 状态,否则任务永远卡在 running" + ) -- 2.54.0 From 7b69872589639eeaec817b41f247d4258bdf9365 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Mon, 24 Aug 2026 07:54:09 +0000 Subject: [PATCH 7/7] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_empty_plan_id_marks_failed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_empty_plan_id_marks_failed.py b/tests/unit/test_empty_plan_id_marks_failed.py index 93fa6e83f..9a6a8cdfe 100644 --- a/tests/unit/test_empty_plan_id_marks_failed.py +++ b/tests/unit/test_empty_plan_id_marks_failed.py @@ -4,8 +4,8 @@ source_edit_plan_id 为空时直接 return failed,但没有调用 mark_failed, 导致 DB 状态永远停在 running。 """ -from pathlib import Path +from pathlib import Path GENERATION_PY = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" @@ -25,7 +25,7 @@ def test_mark_failed_in_else_branch(): assert return_idx != -1, "else 分支中未找到 return 语句" # 关键断言:marker 和 return 之间必须包含 mark_failed - block = source[idx:idx + return_idx] + block = source[idx : idx + return_idx] assert "mark_failed" in block, ( "else 分支在 return 之前必须调用 _update_task_status(task_id, " "'mark_failed', ...) 以更新 DB 状态,否则任务永远卡在 running" -- 2.54.0