阶段 当前状态 说明 Phase 0~3 基本完成 Document IR、证据链、人工确认、不可变 PlanData Snapshot、海报/PPT 投影已实现 Phase 4 代码完成 场景、策略、模板版本,校验/发布门禁,确定性 resolver 和严格槽位合并已实现 Phase 5 代码完成 输入冻结、PPTX/海报对账、失败关闭、幂等、心跳、重试和冻结输入重放已实现 Phase 6 基础完成 质量看板、结构化告警、Golden 审批、留存清理、孤儿检查和灰度开关已实现 正式上线 未完成 缺真实样本、生产模板、业务规则签字和灰度观察 目前验证基线: PPT/海报专项测试:107 passed, 1 skipped Vue TypeScript 检查:通过 前端生产构建:通过 代码变更仍在工作区,尚未提交 仓库全量测试仍有既有失败/挂起项,暂时不能宣称全仓测试完全绿色 仍未完成的代码任务主要有: 影子解析差异流水线 目前有灰度开关,但还没有完整的“新旧解析同时运行、字段差异入库、按保司/profile 聚合”的影子比较任务。 自动视觉回归 目前实现的是 DOM 模块、溢出、尺寸、文本和数值检查;还缺基于真实模板和标准图片的像素差异、字体缺失、遮挡和裁切回归。 告警通道接入 后台已经能产生结构化质量告警,但尚未自动推送到邮件、企微或其他通知通道。 留存任务生产化 dry-run、实删服务和失败审计已经具备,但尚未接入周期性 Celery/定时任务,也没有自动重试失败清理批次。 旧链路最终下线 旧 PPT 解析器和海报紧凑解析仍保留为回滚路径。需要全量灰度稳定后才能删除或彻底关闭写入口。 全仓测试收口 需要处理现有无关失败和挂起测试,建立真正全绿的 CI 基线。 仍需外部输入和生产环境完成的事项: 至少 30 份脱敏 Golden PDF,并完成双人标注和精确率验收。 业务专家确认派生公式、缺失值、可比较性和结论策略。 上传并标注真实生产 PPTX 的语义 shape、页面类型和容量。 安装生产字体并建立视觉基准图片。 实际执行数据库迁移 038~040。 完成留存 dry-run、实删演练以及 10% → 30% → 100% 灰度。 观察期通过后开启 SCENARIO_ENGINE_V2,目前它仍默认关闭;真实清理开关也默认关闭。 完整状态记录在 [PPT与海报Phase4至6补充实施记录](D:/work/code/python/coding/baodanagent/docs/PPT与海报Phase4至6补充实施记录_20260802.md)。
103 lines
4.5 KiB
Python
103 lines
4.5 KiB
Python
"""最终海报文件校验与画布分流回归测试。"""
|
|
import io
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "api"))
|
|
|
|
|
|
def _png(width: int, height: int) -> bytes:
|
|
buffer = io.BytesIO()
|
|
Image.new("RGB", (width, height), "white").save(buffer, format="PNG")
|
|
return buffer.getvalue()
|
|
|
|
|
|
def test_single_rendered_png_requires_exact_output_dimensions():
|
|
from insurance.poster.render_validation import validate_rendered_png
|
|
|
|
result = validate_rendered_png(_png(1024, 1536), "single_2_3")
|
|
assert result == {"width": 1024, "height": 1536, "format": "PNG"}
|
|
|
|
|
|
def test_wrong_dimensions_and_corrupt_png_are_rejected():
|
|
from insurance.poster.render_validation import PosterRenderValidationError, validate_rendered_png
|
|
|
|
with pytest.raises(PosterRenderValidationError, match="尺寸不匹配"):
|
|
validate_rendered_png(_png(1024, 1400), "single_2_3")
|
|
with pytest.raises(PosterRenderValidationError, match="无法解码"):
|
|
validate_rendered_png(b"\x89PNG\r\n\x1a\nnot-a-real-png", "single_2_3")
|
|
|
|
|
|
def test_long_rendered_png_requires_exact_width_and_safe_height():
|
|
from insurance.poster.render_validation import PosterRenderValidationError, validate_rendered_png
|
|
|
|
assert validate_rendered_png(_png(1242, 3600), "long_1242_auto")["height"] == 3600
|
|
with pytest.raises(PosterRenderValidationError, match="宽度不匹配"):
|
|
validate_rendered_png(_png(1080, 3600), "long_1242_auto")
|
|
|
|
|
|
def test_custom_long_height_requires_exact_output_height():
|
|
from insurance.poster.render_validation import PosterRenderValidationError, validate_rendered_png
|
|
|
|
result = validate_rendered_png(_png(1242, 4500), "long_1242_auto", expected_height=4500)
|
|
assert result["height"] == 4500
|
|
with pytest.raises(PosterRenderValidationError, match="自定义高度"):
|
|
validate_rendered_png(_png(1242, 4600), "long_1242_auto", expected_height=4500)
|
|
|
|
|
|
def test_single_and_long_use_separate_canvas_components():
|
|
stage = (ROOT / "frontend/src/components/poster/workspace/PosterStage.vue").read_text(encoding="utf-8")
|
|
single_canvas = ROOT / "frontend/src/components/poster/single/PosterSingleCanvas.vue"
|
|
|
|
assert single_canvas.exists()
|
|
assert "<PosterSingleCanvas" in stage
|
|
assert "v-if=\"draft.outputMode === 'single'\"" in stage
|
|
assert "<PosterHtmlCanvas" in stage
|
|
|
|
|
|
def test_long_editor_exposes_custom_height_without_silent_crop():
|
|
creative = (ROOT / "frontend/src/components/poster/workspace/PosterCreativePanel.vue").read_text(encoding="utf-8")
|
|
stage = (ROOT / "frontend/src/components/poster/workspace/PosterStage.vue").read_text(encoding="utf-8")
|
|
exporter = (ROOT / "frontend/src/utils/poster-exporter.ts").read_text(encoding="utf-8")
|
|
|
|
assert "自定义高度" in creative
|
|
assert ':custom-output-height="draft.longHeightMode === \'custom\' ? draft.customLongHeight : null"' in stage
|
|
assert "当前内容超出自定义高度" in exporter
|
|
|
|
|
|
def test_background_generation_does_not_claim_final_poster_is_done():
|
|
celery_source = (ROOT / "api/insurance/generation/celery_tasks.py").read_text(encoding="utf-8")
|
|
service_source = (ROOT / "api/insurance/poster/service.py").read_text(encoding="utf-8")
|
|
|
|
assert 'record.task_status = "background_ready"' in celery_source
|
|
assert 'record.task_status = "done"' in service_source
|
|
|
|
|
|
def test_failed_client_composition_reuses_existing_background_and_exposes_stage_error():
|
|
page = (ROOT / "frontend/src/pages/PosterPage.vue").read_text(encoding="utf-8")
|
|
exporter = (ROOT / "frontend/src/utils/poster-exporter.ts").read_text(encoding="utf-8")
|
|
|
|
assert "retryExistingComposite" in page
|
|
assert "系统不会重新生成 AI 底图" in page
|
|
assert "BACKGROUND_DECODE_FAILED" in page
|
|
assert "ASSET_WAIT_FAILED" in page
|
|
assert "RENDER_UPLOAD_FAILED" in page
|
|
assert "CHART_RENDER_TIMEOUT" in exporter
|
|
assert "OUTPUT_HEIGHT_UNSAFE" in exporter
|
|
assert "cacheBust: false" in exporter
|
|
|
|
|
|
def test_final_client_export_submits_dom_manifest_before_download():
|
|
page = (ROOT / "frontend/src/pages/PosterPage.vue").read_text(encoding="utf-8")
|
|
exporter = (ROOT / "frontend/src/utils/poster-exporter.ts").read_text(encoding="utf-8")
|
|
routes = (ROOT / "api/insurance/poster/routes.py").read_text(encoding="utf-8")
|
|
|
|
assert "collectPosterExportManifest" in page
|
|
assert "poster-export-manifest-v1" in exporter
|
|
assert "data-poster-section" in exporter
|
|
assert 'request.form.get("exportManifest")' in routes
|