From 084c0494b7bbc16b05098c363d459aec8286da04 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 23 Aug 2026 18:56:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(worker):=20P1=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=8A=B6=E6=80=81=E8=A2=AB=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E8=A6=86=E7=9B=96=E5=9B=9Epending=20+=20?= =?UTF-8?q?=E5=B0=81=E9=9D=A2metadata=E5=B1=9E=E6=80=A7=E5=90=8D=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug1: asset_analyses 持久化时使用任务开头加载的 gen_task 内存对象 (status=pending) 整体 _repo.update(),把已被 _update_task_status 改为 running 的 DB status 覆盖回 pending,导致 mark_completed 报非法状态转换。 改为独立 session 查询最新模型,只更新 extra_meta 字段。 Bug2: 封面候选帧持久化使用 _cover_model.metadata(SQLAlchemy 声明基类 保留的 MetaData 对象),赋值无效;ORM 属性名应为 extra_meta(DB列名 为 metadata)。两处全部修正。 新增 5 个防回归单测覆盖两个 Bug。 --- apps/worker/worker_app/tasks/generation.py | 32 +++++--- .../unit/test_worker_cover_meta_and_status.py | 77 +++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 tests/unit/test_worker_cover_meta_and_status.py diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index a2f211170..bb3b52574 100644 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -1493,9 +1493,9 @@ def generate_video(self, task_id: str) -> dict: ) if _cover_model: _cover_model.cover_url = cover_frame_url - meta = dict(_cover_model.metadata or {}) + meta = dict(_cover_model.extra_meta or {}) meta["cover_candidates"] = cover_candidates - _cover_model.metadata = meta + _cover_model.extra_meta = meta _cover_session.commit() finally: _cover_session.close() @@ -1680,10 +1680,24 @@ def generate_video(self, task_id: str) -> dict: logger.info("[task_id=%s] MediaKit 视频理解完成: %d 个素材", task_id, len(asset_urls)) # 保存分析结果到 extra_meta - if asset_analyses and gen_task: - gen_task.extra_meta = {**(gen_task.extra_meta or {}), "asset_analyses": asset_analyses} - _repo.update(gen_task) - _flush_logs(task_id, gen_task) + if asset_analyses: + _meta_session = SessionLocal() + try: + from packages.adapters.sqlalchemy_impl.models import ( + GenerationTaskModel, + ) + _m = _meta_session.query(GenerationTaskModel).filter( + GenerationTaskModel.id == task_id + ).first() + if _m: + existing = dict(_m.extra_meta or {}) + existing["asset_analyses"] = asset_analyses + _m.extra_meta = existing + _meta_session.commit() + finally: + _meta_session.close() + if gen_task: + _flush_logs(task_id, gen_task) except Exception: logger.warning("[task_id=%s] MediaKit 视频理解失败,继续渲染", task_id, exc_info=True) @@ -1777,10 +1791,10 @@ def generate_video(self, task_id: str) -> dict: ) if _cover_model: _cover_model.cover_url = cover_frame_url - # 持久化完整候选列表到 metadata - meta = dict(_cover_model.metadata or {}) + # 持久化完整候选列表到 extra_meta + meta = dict(_cover_model.extra_meta or {}) meta["cover_candidates"] = cover_candidates - _cover_model.metadata = meta + _cover_model.extra_meta = meta _cover_session.commit() logger.info( "[task_id=%s] 封面帧已持久化(ffmpeg本地抽帧): cover_url=%s candidates=%d", diff --git a/tests/unit/test_worker_cover_meta_and_status.py b/tests/unit/test_worker_cover_meta_and_status.py new file mode 100644 index 000000000..f93a98b78 --- /dev/null +++ b/tests/unit/test_worker_cover_meta_and_status.py @@ -0,0 +1,77 @@ +"""防回归测试:P1 修复 +- Bug 1: gen_task 过期内存对象 _repo.update() 覆盖 DB status 为 pending +- Bug 2: 封面模型误用 .metadata(SQLAlchemy 保留属性),应为 .extra_meta +""" +import ast +from pathlib import Path + +import pytest + + +GENERATION_FILE = ( + Path(__file__).resolve().parents[2] + / "apps" + / "worker" + / "worker_app" + / "tasks" + / "generation.py" +) + + +def _read_source() -> str: + return GENERATION_FILE.read_text(encoding="utf-8") + + +class TestCoverModelUsesExtraMeta: + """封面持久化必须使用 ORM 属性 extra_meta,而不是 SQLAlchemy 保留的 .metadata。""" + + def test_no_metadata_attribute_access_on_cover_model(self): + source = _read_source() + # 禁止对 _cover_model.metadata 进行读或写 + assert "_cover_model.metadata" not in source, ( + "_cover_model.metadata is the SQLAlchemy reserved MetaData object, " + "not the JSON column. Use _cover_model.extra_meta instead." + ) + + def test_extra_meta_used_for_cover_candidates(self): + source = _read_source() + assert "_cover_model.extra_meta" in source + assert 'meta["cover_candidates"]' in source + + +class TestAssetAnalysesDoesNotOverwriteStatus: + """保存 asset_analyses 时不能用过期的 gen_task 内存对象整体 _repo.update, + 否则会把已被 _update_task_status 改为 running 的 status 覆盖回 pending。""" + + def test_no_stale_repo_update_with_gen_task(self): + source = _read_source() + # 旧代码:gen_task.extra_meta = {...}; _repo.update(gen_task) + # 这行会把内存中的 pending status 写回 DB + assert "_repo.update(gen_task)" not in source, ( + "_repo.update(gen_task) writes a stale in-memory object back to DB, " + "overwriting status set by _update_task_status. " + "Use an independent session to update only extra_meta." + ) + + def test_asset_analyses_uses_independent_session(self): + """asset_analyses 持久化必须用独立 session 查询最新模型再提交。""" + source = _read_source() + assert "_meta_session" in source + assert "GenerationTaskModel" in source + # 必须只更新 extra_meta 字段 + assert 'existing["asset_analyses"]' in source + + +class TestGenerationTaskModelOrmAttribute: + """确认 ORM 属性映射:Python 属性 extra_meta -> DB 列 metadata。""" + + def test_orm_attribute_is_extra_meta(self): + from packages.adapters.sqlalchemy_impl.models import GenerationTaskModel + + # ORM 属性必须存在 + assert hasattr(GenerationTaskModel, "extra_meta") + # .metadata 是 SQLAlchemy 声明基类保留的 MetaData,不是列描述符 + # 它不应该是我们的 JSON 字段 + from sqlalchemy import MetaData + + assert isinstance(GenerationTaskModel.metadata, MetaData) -- 2.54.0 From 0ecf27a8d042e987b2bd5977083b4f7d7ad12c4b Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sun, 23 Aug 2026 10:59:14 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- apps/worker/worker_app/tasks/generation.py | 9 ++++++--- tests/unit/test_worker_cover_meta_and_status.py | 11 ++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/worker/worker_app/tasks/generation.py b/apps/worker/worker_app/tasks/generation.py index bb3b52574..a089726af 100644 --- a/apps/worker/worker_app/tasks/generation.py +++ b/apps/worker/worker_app/tasks/generation.py @@ -1686,9 +1686,12 @@ def generate_video(self, task_id: str) -> dict: from packages.adapters.sqlalchemy_impl.models import ( GenerationTaskModel, ) - _m = _meta_session.query(GenerationTaskModel).filter( - GenerationTaskModel.id == task_id - ).first() + + _m = ( + _meta_session.query(GenerationTaskModel) + .filter(GenerationTaskModel.id == task_id) + .first() + ) if _m: existing = dict(_m.extra_meta or {}) existing["asset_analyses"] = asset_analyses diff --git a/tests/unit/test_worker_cover_meta_and_status.py b/tests/unit/test_worker_cover_meta_and_status.py index f93a98b78..9d6fa1ed3 100644 --- a/tests/unit/test_worker_cover_meta_and_status.py +++ b/tests/unit/test_worker_cover_meta_and_status.py @@ -2,20 +2,13 @@ - Bug 1: gen_task 过期内存对象 _repo.update() 覆盖 DB status 为 pending - Bug 2: 封面模型误用 .metadata(SQLAlchemy 保留属性),应为 .extra_meta """ + import ast from pathlib import Path import pytest - -GENERATION_FILE = ( - Path(__file__).resolve().parents[2] - / "apps" - / "worker" - / "worker_app" - / "tasks" - / "generation.py" -) +GENERATION_FILE = Path(__file__).resolve().parents[2] / "apps" / "worker" / "worker_app" / "tasks" / "generation.py" def _read_source() -> str: -- 2.54.0