30094bc591
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Successful in 6m31s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 6m50s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 8m12s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 8m19s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 43h50m31s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 43h50m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 43h51m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 43h50m21s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 43h49m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 43h49m59s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 43h50m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 43h50m3s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 43h50m8s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 43h50m8s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 43h49m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 43h49m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 43h51m28s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 43h52m50s
问题:plan.config中标题配置字段名双轨不一致——Worker sync_configs_to_plan 写 plan.config['title'](归一化 size/font/color),API writeback_edit_plan_config 写 plan.config['title_config'](不归一化),三个渲染引擎读取不一致: - video_compose_service(FFmpeg drawtext):已读 title+fallback title_config ✅ - unified_render_service(ASS字幕):只 title 无 fallback ❌ - subtitle_render_engine(字幕渲染):只读 title_config ❌ 修复: - packages/application/generation_common.py writeback_edit_plan_config:统一写 plan.config['title'](而非 title_config),并做字段名归一化 font_size→size/font_preset→font/font_color→color,与 Worker sync_configs_to_plan 行为一致 - apps/worker/video_processing/subtitle_render_engine.py:_resolve_title_config 先读 plan.config['title'],fallback plan.config['title_config'](兼容存量老数据) - apps/worker/video_processing/unified_render_service.py:同样先读 'title',text 为空时 fallback 'title_config' - apps/api/app/api/routes/generation_cover.py:封面E2标题配置读取加 title_config fallback - 单测适配:test_generation_common.py/test_writeback_edit_plan_config.py 断言改 'title' 并验证归一化 - 所有渲染路径(智能剪辑/AI数字人/封面E2)统一读 'title',老数据通过 fallback 继续渲染
131 lines
5.0 KiB
Python
131 lines
5.0 KiB
Python
"""Tests for _writeback_edit_plan_config in generation_tasks route.
|
||
|
||
覆盖 CI 增量覆盖率不足的代码:
|
||
- generation_tasks.py 行 160-193 (_writeback_edit_plan_config 函数体)
|
||
- generation_tasks.py 行 371-372 (路由中调用该函数)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
||
|
||
from app.api.routes.generation_tasks import _writeback_edit_plan_config
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_db():
|
||
"""Mock SQLAlchemy Session."""
|
||
db = MagicMock()
|
||
db.query.return_value = db
|
||
db.filter.return_value = db
|
||
return db
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_plan():
|
||
"""Mock EditPlanModel instance."""
|
||
plan = MagicMock()
|
||
plan.config = {"existing_key": "existing_value"}
|
||
return plan
|
||
|
||
|
||
class TestWritebackEditPlanConfig:
|
||
"""_writeback_edit_plan_config 全分支覆盖"""
|
||
|
||
# ---- 行 160-161: plan_id 为空直接返回 ----
|
||
def test_empty_plan_id_returns_immediately(self, mock_db):
|
||
_writeback_edit_plan_config(plan_id="", task_id="task_1", title_config={"text": "hi"}, db=mock_db)
|
||
mock_db.query.assert_not_called()
|
||
mock_db.commit.assert_not_called()
|
||
|
||
def test_none_plan_id_returns_immediately(self, mock_db):
|
||
_writeback_edit_plan_config(plan_id=None, task_id="task_1", title_config=None, db=mock_db)
|
||
mock_db.query.assert_not_called()
|
||
|
||
# ---- 行 165-168: plan 不存在 → warning + 不 commit ----
|
||
def test_plan_not_found_no_commit(self, mock_db):
|
||
mock_db.first.return_value = None
|
||
|
||
_writeback_edit_plan_config(plan_id="plan_999", task_id="task_1", title_config=None, db=mock_db)
|
||
|
||
mock_db.query.assert_called_once()
|
||
mock_db.commit.assert_not_called()
|
||
|
||
# ---- 行 170-182: 正常写入 + title(#1901 统一字段名) ----
|
||
def test_success_with_title_config(self, mock_db, mock_plan):
|
||
mock_db.first.return_value = mock_plan
|
||
|
||
_writeback_edit_plan_config(
|
||
plan_id="plan_123",
|
||
task_id="task_456",
|
||
title_config={"text": "标题", "font_size": 36},
|
||
db=mock_db,
|
||
)
|
||
|
||
assert mock_plan.config["generation_task_id"] == "task_456"
|
||
# #1901: 统一写到 "title" 字段,且 font_size 已归一化为 size
|
||
assert mock_plan.config["title"]["text"] == "标题"
|
||
assert mock_plan.config["title"]["size"] == 36
|
||
assert mock_plan.config["title"]["font_size"] == 36
|
||
assert "title_config" not in mock_plan.config
|
||
assert mock_plan.config["existing_key"] == "existing_value"
|
||
mock_db.commit.assert_called_once()
|
||
|
||
# ---- 行 170-175: 正常写入、无 title_config ----
|
||
def test_success_without_title_config(self, mock_db, mock_plan):
|
||
mock_db.first.return_value = mock_plan
|
||
|
||
_writeback_edit_plan_config(plan_id="plan_123", task_id="task_789", title_config=None, db=mock_db)
|
||
|
||
assert mock_plan.config["generation_task_id"] == "task_789"
|
||
assert "title" not in mock_plan.config or not mock_plan.config.get("title")
|
||
mock_db.commit.assert_called_once()
|
||
|
||
# ---- 行 170: config 不是 dict → 兜底空 dict ----
|
||
def test_config_not_dict_uses_empty_dict(self, mock_db):
|
||
bad_plan = MagicMock()
|
||
bad_plan.config = "not_a_dict"
|
||
mock_db.first.return_value = bad_plan
|
||
|
||
_writeback_edit_plan_config(plan_id="plan_123", task_id="task_1", title_config=None, db=mock_db)
|
||
|
||
assert isinstance(bad_plan.config, dict)
|
||
assert bad_plan.config["generation_task_id"] == "task_1"
|
||
mock_db.commit.assert_called_once()
|
||
|
||
# ---- 行 183-189: DB 异常 → warning + rollback ----
|
||
def test_db_exception_triggers_rollback(self, mock_db, mock_plan):
|
||
mock_db.first.return_value = mock_plan
|
||
mock_db.commit.side_effect = RuntimeError("DB connection lost")
|
||
|
||
# 不应抛异常
|
||
_writeback_edit_plan_config(plan_id="plan_123", task_id="task_1", title_config=None, db=mock_db)
|
||
|
||
mock_db.rollback.assert_called_once()
|
||
|
||
# ---- 行 190-193: rollback 也失败 → 静默 ----
|
||
def test_rollback_failure_silent(self, mock_db, mock_plan):
|
||
mock_db.first.return_value = mock_plan
|
||
mock_db.commit.side_effect = RuntimeError("commit failed")
|
||
mock_db.rollback.side_effect = RuntimeError("rollback also failed")
|
||
|
||
# 两个异常都不应抛出
|
||
_writeback_edit_plan_config(plan_id="plan_123", task_id="task_1", title_config=None, db=mock_db)
|
||
mock_db.rollback.assert_called_once()
|
||
|
||
# ---- 行 173: title_config 为空 dict → 不写入 title ----
|
||
def test_empty_title_config_not_written(self, mock_db, mock_plan):
|
||
mock_db.first.return_value = mock_plan
|
||
|
||
_writeback_edit_plan_config(plan_id="plan_123", task_id="task_1", title_config={}, db=mock_db)
|
||
|
||
# 空 dict 为 falsy,不写入
|
||
assert "title" not in mock_plan.config or not mock_plan.config.get("title")
|
||
assert mock_plan.config["generation_task_id"] == "task_1"
|