Files
xiaoxia-saas/tests/unit/test_writeback_edit_plan_config.py
saas-backend-agent 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
fix(backend): #1901 统一title字段名修复标题渲染不一致
问题: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 继续渲染
2026-09-14 19:27:29 +08:00

131 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"