baodan/tests/ppt_poster_optimization_test.py
wsb1224 caee27b0d3 主要修复:
海报加载失败根因:html-to-image 给 blob: 底图地址追加缓存参数,导致地址失效。现已关闭该行为。
合成失败后再次点击“重新生成”,会复用已有 AI 底图,只重试浏览器合成和上传,避免重复调用 AI。
增加底图加载、图表超时、导出失败、尺寸越界等分阶段错误提示。
修复计划书 (cid:数字) 字体乱码被误判为正常文本的问题,现在会正确转入 OCR。
增加繁体中文 OCR 运行支持。
补齐 SIUL 文件名中的确定字段,并且不会覆盖正文已识别数据。
增加“首期规划保费/償還至形成基金所需保費”等保费标签识别。
修正 IUL 年龄、保额、退保价值、缴费期、公司信息等字段映射。
LLM 返回空对象或缺字段时不再视为成功。
增加错误利益数值和年龄/保单年度错位校验。
修复依赖版本降级导致 API/Worker 无法启动的风险。
真实计划书复验结果:
产品:Manulife SIUL 3
投保年龄:48 岁
性别:女性
吸烟状态:非吸烟
币种:USD
基本保额:3,000,000
年缴保费:80,060
缴费期:5 年
利益演示:识别到 10 行
验证结果:
后端相关回归测试:91 passed
前端生产构建:通过
API、数据库、Redis、存储、数据表健康检查:全部正常
Celery Worker:已重启并连接 Redis
真实 PDF:确认进入 OCR,不再使用 (cid:...) 乱码
前端构建目录由运行容器挂载,修复已生效
2026-07-31 23:35:45 +08:00

295 lines
10 KiB
Python

"""PPT/海报优化规则回归测试。"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
def test_currency_aliases_and_unknown_are_not_fabricated():
from insurance.ppt.normalizer import _normalize_currency
assert _normalize_currency("USB") == "USD"
assert _normalize_currency("RMB") == "CNY"
assert _normalize_currency("") == "CNY"
assert _normalize_currency("XYZ") is None
assert _normalize_currency(None) is None
def test_optional_review_fields_are_normalized_without_becoming_required():
from insurance.ppt.normalizer import normalize_savings_plan
plan = normalize_savings_plan({
"product_name": "储蓄计划",
"insured": {"age": 35, "gender": "", "smoker": ""},
"policy": {
"currency": "RMB",
"annual_premium": 10000,
"premium_payment_period": 5,
"basic_plan_annual_premium": 9000,
"basic_sum_insured": 500000,
"first_year_amount_due": 9800,
},
})
assert plan["insured"]["smoker"] == "no"
assert plan["policy"]["currency"] == "CNY"
assert plan["policy"]["basicPlanAnnualPremium"] == 9000
assert plan["policy"]["basicSumInsured"] == 500000
assert plan["policy"]["firstYearAmountDue"] == 9800
def test_iul_key_fields_accept_raw_and_normalized_names_without_data_loss():
from insurance.ppt.normalizer import normalize_iul_plan
plan = normalize_iul_plan({
"productName": "Manulife SIUL 3",
"insured": {"age": 48, "gender": "female", "smoker": "non-smoker"},
"policy": {
"currency": "USD",
"sumInsured": 3000000,
"annualPremium": 80060,
"payYears": 5,
"coverage_period": "终身",
},
"benefit_illustration": [
{"policy_year": 1, "total_surrender_value": 31600},
{"policy_year": 10, "total_surrender_value": 76800},
],
})
assert plan["productName"] == "Manulife SIUL 3"
assert plan["insured"]["age"] == 48
assert plan["insured"]["smoker"] == "no"
assert plan["policy"]["sumInsured"] == 3000000
assert plan["policy"]["annualPremium"] == 80060
assert plan["policy"]["payYears"] == 5
assert plan["benefitRows"][0]["age"] == 48
assert plan["benefitRows"][1]["age"] == 57
assert plan["benefitRows"][1]["totalSurrenderValue"] == 76800
def test_iul_implausibly_tiny_benefit_values_block_generation():
from insurance.ppt.validator import validate_formal_iul_plan
issues = validate_formal_iul_plan({
"productName": "Manulife SIUL 3",
"insured": {"age": 48, "smoker": "no"},
"policy": {
"currency": "USD", "sumInsured": 3000000,
"annualPremium": 80060, "paymentPeriod": "5",
},
"indexAccounts": [{"name": "S&P 500"}],
"benefitRows": [
{"policyYear": year, "age": 48 + year - 1, "totalSurrenderValue": value}
for year, value in [(1, 56), (10, 85), (20, 114), (30, 122)]
],
"source": {"pdfHash": "hash"},
})
assert any(
issue.code == "IUL_BENEFIT_VALUE_IMPLAUSIBLE" and issue.level == "error"
for issue in issues
)
def test_ppt_generation_preserves_company_selected_for_uploaded_file():
source = (
Path(__file__).resolve().parents[1]
/ "api/insurance/generation/celery_tasks.py"
).read_text(encoding="utf-8")
assert 'normalized["companyId"] = ext.get("companyId") or company_id or ""' in source
assert "if not company_id:" in source
def test_benefit_and_withdrawal_issues_never_block_savings_generation():
from insurance.ppt.validator import validate_formal_savings_plan
issues = validate_formal_savings_plan({
"productName": "储蓄计划",
"insured": {"age": 35, "smoker": "no"},
"policy": {"currency": "USD", "annualPremium": 10000, "payYears": 5},
"benefitRows": [
{
"policyYear": 1,
"guaranteedCashValue": 100,
"totalSurrenderValue": 50,
}
],
"withdrawalRows": [{"policyYear": 1}, {"policyYear": 3}],
"source": {"pdfHash": "hash"},
})
affected = [
issue for issue in issues
if issue.section in ("benefitRows", "withdrawalRows")
]
assert affected
assert all(issue.level == "warn" for issue in affected)
def test_brand_policy_controls_company_product_and_logo_independently():
from insurance.ppt.masking import apply_brand_policy, build_brand_policy
company = {
"id": "c1",
"displayName": "真实保司",
"maskedDisplayName": "保X",
"maskingEnabled": True,
"logoEnabled": False,
"logoUrl": "/logo.png",
}
product = {
"id": "p1",
"displayName": "真实产品",
"maskedDisplayName": "产X",
"maskingEnabled": False,
}
policy = build_brand_policy(company, [product])
masked_company, masked_product = apply_brand_policy(company, product, policy)
assert masked_company["displayName"] == "保X"
assert masked_company["logoUrl"] == ""
assert masked_product["displayName"] == "真实产品"
def test_all_six_core_poster_profiles_have_distinct_content_budgets():
from insurance.poster.field_profiles import get_field_profile
profiles = {
(plan_type, output_mode): get_field_profile(plan_type, output_mode)
for plan_type in ("savings", "ci", "iul")
for output_mode in ("single", "long")
}
assert len(profiles) == 6
for plan_type in ("savings", "ci", "iul"):
assert profiles[(plan_type, "single")]["maxFeatureCount"] == 3
assert profiles[(plan_type, "long")]["maxFeatureCount"] == 6
def test_single_poster_never_contains_full_benefit_table():
from insurance.poster.content_builder import build_poster_content
rows = [
{"policy_year": year, "total_surrender": year * 1000}
for year in range(1, 31)
]
content = build_poster_content(
{"benefit_illustration": rows},
{"features": [{"title": str(index)} for index in range(8)]},
output_mode="single",
plan_type="savings",
)
assert len(content["benefit_table"]) <= 1
assert len(content["features"]) <= 3
def test_custom_scenario_is_compatible_only_with_its_calculation_mode(monkeypatch):
from insurance.ppt import scenarios
monkeypatch.setattr(scenarios, "get_scenario_config", lambda _code: {
"generationMode": "single",
"baseScenario": None,
})
assert scenarios.template_scenario_compatible("retirement", "generic_single")
assert not scenarios.template_scenario_compatible("retirement", "generic_compare")
def test_poster_case_mapping_reads_nested_insured_and_policy_fields():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35, "gender": "female"},
"policy": {
"currency": "RMB",
"sum_insured": 500000,
"annual_premium": 100000,
"premium_payment_period": 5,
"coverage_period": "终身",
},
"benefit_illustration": [{"policy_year": 10, "total_surrender_value": 800000}],
}, "savings", "success")
assert mapped["age"] == 35
assert mapped["gender"] == ""
assert mapped["currency"] == "CNY"
assert mapped["sum_assured"] == 500000
assert mapped["annual_premium"] == 100000
assert mapped["premium_term"] == 5
assert mapped["coverage_period"] == "终身"
assert mapped["benefit_table"][0]["policy_year"] == 10
assert mapped["meta"]["status"] == "parsed"
def test_poster_case_mapping_does_not_report_empty_result_as_parsed():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({}, "savings", "partial")
assert mapped["meta"]["status"] == "failed"
assert mapped["meta"]["validFieldCount"] == 0
assert "age" in mapped["meta"]["missingFields"]
def test_poster_compliance_returns_all_character_ranges_and_revision():
from insurance.poster.compliance import check_copy_compliance
result = check_copy_compliance({
"headline": "保证收益",
"body": "并非无风险,也不是零风险。",
"call_to_action": "立即咨询",
})
assert result["status"] == "block"
assert result["revision"]
assert [(issue["field"], issue["text"]) for issue in result["issues"]] == [
("headline", "保证"),
("body", "无风险"),
("body", "零风险"),
]
assert result["issues"][0]["start"] == 0
assert result["issues"][0]["end"] == 2
def test_poster_compliance_warns_and_returns_direct_replacement():
from insurance.poster.compliance import check_copy_compliance
result = check_copy_compliance({
"headline": "行业领先的保障方案",
"body": "具体内容以正式合同为准。",
"call_to_action": "了解详情",
})
assert result["status"] == "warn"
assert result["issues"][0]["severity"] == "warn"
assert result["issues"][0]["replacement"] == "具有特色"
def test_poster_generation_requires_template_before_other_processing():
from insurance.poster.service import PosterService
result = PosterService().generate_poster("user-1", {
"copyContent": {
"headline": "保障方案",
"body": "具体内容以正式合同为准。",
"call_to_action": "了解详情",
},
})
assert result == {"code": 1001, "message": "请选择海报模板", "data": None}
def test_poster_case_mapping_preserves_parse_diagnostics():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35},
"policy": {"currency": "USD", "annual_premium": 10000, "premium_payment_period": 5},
"_meta": {"method": "regex+ocr", "low_quality_pages": [3, 8]},
"_provenance": {"insured.age": {"source": "ocr", "confidence": 0.8}},
}, "savings", "partial")
assert mapped["meta"]["method"] == "regex+ocr"
assert mapped["meta"]["lowQualityPages"] == [3, 8]
assert mapped["meta"]["provenance"]["insured.age"]["confidence"] == 0.8