baodan/tests/ppt_poster_optimization_test.py
wsb1224 9dc494d968 主要修改:
利益表按最多两页、9000字符分块调用 LLM,成功分块按保单年度合并、去重、排序。[extraction.py (line 125)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:125)
身份字段只发送关键页面;没有提领关键词时不再发送整份 PDF 做空提领请求。
单个利益分块失败不会丢失其他成功分块,并记录具体分块编号和错误。[extraction.py (line 1161)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:1161)
RemoteProtocolError、连接超时、408/429/部分5xx现在会按配置执行真实退避重试;DeepSeek默认最多调用3次。[llm_client.py (line 600)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/llm_client.py:600)
只有一个供应商时,错误信息会明确显示“已配置1个供应商”,不再误导为存在多个备用供应商。
利益表分块失败或储蓄险年度数据不足时,结果标记为 partial,不再伪装完整成功。[extraction.py (line 750)](D:/work/code/python/coding/baodanagent/api/insurance/ppt/extraction.py:750)
会话显示“解析完成,部分文件需校对”,同时保留可人工修正的数据。
日志增加文件名、文件序号、最终状态、利益行数,以及身份/利益各分块/提领的独立耗时、token和错误信息。[celery_tasks.py (line 181)](D:/work/code/python/coding/baodanagent/api/insurance/generation/celery_tasks.py:181)
缓存版本升级至 v8,旧解析缓存自动失效。
验证结果:
PPT专项测试:54 passed, 1 skipped
大范围测试:231 passed, 1 skipped
仅剩既有聊天日志测试缺少 Flask application context
完整测试收集另受本机缺少 python-pptx 影响
Python语法检查:通过
前端生产构建:通过
git diff --check:通过
2026-08-01 02:25:35 +08:00

726 lines
26 KiB
Python
Raw 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.

"""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_missing_optional_withdrawal_plan_does_not_require_review():
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": year, "sourcePage": 10, "totalSurrenderValue": year * 1000}
for year in range(1, 31)
],
"withdrawalRows": [],
"source": {"pdfHash": "hash"},
})
assert not any(issue.code == "WITHDRAWAL_ROWS_MISSING" for issue in issues)
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_keeps_product_and_surrender_milestones():
from insurance.poster.tasks import _map_extract_plan_fields
mapped = _map_extract_plan_fields({
"insured": {"age": 35, "gender": "female", "smoker_status": "no"},
"policy": {
"currency": "USD", "sum_insured": 500000,
"annual_premium": 10000, "premium_payment_period": 5,
},
"benefit_illustration": [
{"policy_year": 1, "death_benefit": 500000, "total_surrender_value": 1000},
{"policy_year": 10, "total_surrender_value": 31600},
{"policy_year": 20, "total_surrender_value": 76800},
{"policy_year": 30, "total_surrender_value": 129300},
],
}, "savings", "success", product_context={
"productName": "宏挚传承",
"companyName": "宏利",
})
assert mapped["product_name"] == "宏挚传承"
assert mapped["company_name"] == "宏利"
assert mapped["smoking_status"] == "非吸烟"
assert mapped["total_premium"] == 50000
assert mapped["initial_death_benefit"] == 500000
assert mapped["surrender_value_10"] == 31600
assert mapped["surrender_value_20"] == 76800
assert mapped["surrender_value_30"] == 129300
provenance = mapped["meta"]["provenance"]
assert provenance["total_premium"]["source"] == "system_derived"
assert provenance["initial_death_benefit"]["source"] == "system_derived"
assert provenance["surrender_value_20"]["from"] == "benefit_table.year_20"
def test_poster_content_accepts_canonical_snake_case_benefit_fields():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"benefit_illustration": [
{
"policy_year": year,
"guaranteed_cash_value": year * 100,
"total_surrender_value": year * 1000,
}
for year in (10, 20, 30)
],
}, {}, output_mode="long", plan_type="savings")
assert content["benefit_table"][0]["guaranteed"] == 1000
assert content["benefit_table"][0]["totalSurrender"] == 10000
def test_regex_benefit_table_keeps_age_separate_from_money():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称Manulife SIUL 3
受保人年龄47
年缴保费80,060
保单年度 受保人年龄 缴付保费总额 退保总值
1 48 80,060 31,600
8 55 640,480 76,800
""")
first = data["benefit_illustration"][0]
assert first["policy_year"] == 1
assert first["age"] == 48
assert first["total_premium_paid"] == 80060
assert first["total_surrender_value"] == 31600
def test_regex_age_only_table_derives_policy_year_from_explicit_issue_age():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称:储蓄计划
受保人年龄47
年缴保费10,000
受保人年龄 退保价值
48 1,000
49 2,000
""")
rows = data["benefit_illustration"]
assert [row["policy_year"] for row in rows] == [1, 2]
assert [row["age"] for row in rows] == [48, 49]
assert rows[0]["policy_year_source"] == "derived_from_age"
def test_regex_iul_table_preserves_dash_placeholders_without_column_shift():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称Manulife SIUL 3
受保人年龄47
年缴保费80,060
保单年度 年龄 累计保费 保证现金价值 保证账户价值 非保证账户价值 非保证现金价值 退保总值 非保证身故赔偿
37 — 2,962,220 2,000 — — 1,000 85 10,000
44 — 3,522,640 3,000 — — 2,000 92 12,000
""")
row = data["benefit_illustration"][0]
assert row["policy_year"] == 37
assert row["age"] is None
assert row["total_premium_paid"] == 2962220
assert row["guaranteed_cash_value"] == 2000
assert row["guaranteed_account_value"] is None
assert row["non_guaranteed_account_value"] is None
assert row["non_guaranteed_cash_value"] == 1000
assert row["total_surrender_value"] == 85
assert row["non_guaranteed_death_benefit"] == 10000
assert data["benefit_illustration"][1]["total_premium_paid"] == 3522640
assert data["benefit_illustration"][1]["total_surrender_value"] == 92
def test_regex_quality_gate_rejects_age_like_surrender_values():
from insurance.ppt.extraction import _regex_quality_gate
passed, problems = _regex_quality_gate({
"product_name": "Manulife SIUL 3",
"insured": {"age": 47},
"policy": {"sum_insured": 3000000, "annual_premium": 80060},
"index_accounts": [{"name": "S&P 500"}],
"benefit_illustration": [
{"policy_year": year, "total_surrender_value": age}
for year, age in ((1, 49), (8, 56), (37, 85), (44, 92))
],
}, "iul")
assert passed is False
assert "benefit_illustration.total_surrender_value_implausible" in problems
def test_regex_quality_gate_rejects_iul_rows_without_iul_value_columns():
from insurance.ppt.extraction import _regex_quality_gate
passed, problems = _regex_quality_gate({
"product_name": "Manulife SIUL 3",
"insured": {"age": 47},
"policy": {"sum_insured": 3000000, "annual_premium": 80060},
"index_accounts": [{"name": "S&P 500"}],
"benefit_illustration": [
{"policy_year": year, "total_premium_paid": 80060 * year, "total_surrender_value": 10000 * year}
for year in (1, 8, 20)
],
}, "iul")
assert passed is False
assert "benefit_illustration.iul_value_columns" in problems
def test_iul_split_fallback_requests_iul_specific_fields():
import asyncio
from types import SimpleNamespace
from insurance.ppt.extraction import _llm_extract_split
prompts = []
class FakeClient:
async def structured_output(self, **kwargs):
prompt = kwargs["prompt"]
prompts.append(prompt)
if "身份和保单字段" in prompt:
return {
"product_name": "SIUL",
"insured": {"age": 47, "gender": "male"},
"policy": {"sum_insured": 3000000},
"index_accounts": [{"name": "S&P 500"}],
}, SimpleNamespace(tokens={})
if "利益演示表" in prompt:
return {"benefit_illustration": []}, SimpleNamespace(tokens={})
return {"withdrawal_illustration": []}, SimpleNamespace(tokens={})
asyncio.run(_llm_extract_split(
"[PAGE 1]\nIUL 计划书\n\n"
"[PAGE 2]\n提领方案 保单年度 提取金额 退保价值",
"iul",
FakeClient(),
))
assert "index_accounts" in prompts[0]
assert "guaranteed_account_value" in prompts[1]
assert "non_guaranteed_cash_value" in prompts[1]
assert "non_guaranteed_death_benefit" in prompts[1]
assert "根对象只能包含 benefit_illustration 字段" in prompts[1]
assert "根对象只能包含 withdrawal_illustration 字段" in prompts[2]
def test_split_extraction_chunks_benefit_pages_and_merges_rows():
import asyncio
import re
from types import SimpleNamespace
from insurance.ppt.extraction import _llm_extract_split
prompts = []
class FakeClient:
async def structured_output(self, **kwargs):
prompt = kwargs["prompt"]
prompts.append(prompt)
response = SimpleNamespace(tokens={"input": 10, "output": 5}, latency_ms=10)
if "身份和保单字段" in prompt:
return {
"product_name": "储蓄计划",
"insured": {"age": 35, "gender": "female"},
"policy": {"currency": "USD", "sum_insured": None,
"annual_premium": 10000, "premium_payment_period": 5},
}, response
page_numbers = [int(value) for value in re.findall(r"\[PAGE (\d+)\]", prompt)]
return {
"benefit_illustration": [
{"policy_year": page, "total_surrender_value": page * 1000, "source_page": page}
for page in page_numbers
],
}, response
pdf_text = "[PAGE 1]\n产品名称:储蓄计划\n受保人年龄35\n"
pdf_text += "\n\n".join(
f"[PAGE {page}]\n保单年度 保证现金价值 退保价值\n{page} 100 {page * 1000}"
for page in range(2, 7)
)
data, _response = asyncio.run(_llm_extract_split(pdf_text, "savings", FakeClient()))
benefit_prompts = [
prompt for prompt in prompts
if "利益演示表" in prompt and "当前分块" in prompt
]
assert len(benefit_prompts) == 3
assert [row["policy_year"] for row in data["benefit_illustration"]] == [2, 3, 4, 5, 6]
assert data["_meta"]["split_extraction"]["benefit"]["status"] == "success"
assert data["_meta"]["split_extraction"]["benefit"]["chunkCount"] == 3
def test_incomplete_savings_benefit_rows_are_partial():
from insurance.ppt.extraction import assess_extraction_payload
status, message = assess_extraction_payload({
"product_name": "储蓄计划",
"insured": {"age": 35},
"policy": {"annual_premium": 10000},
"benefit_illustration": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (1, 10, 20)
],
"_meta": {
"split_extraction": {
"benefit": {"status": "failed", "errors": ["RemoteProtocolError"]},
},
},
}, "savings")
assert status == "partial"
assert "利益演示数据不足" in message
def test_llm_client_retries_retryable_transport_error(monkeypatch):
import asyncio
import httpx
from insurance.ppt import llm_client as llm_module
client = llm_module.LLMClient.__new__(llm_module.LLMClient)
config = llm_module.LLMProviderConfig(
name="deepseek", base_url="https://example.com/v1", model="model", max_retries=2,
)
client._configs = [(config, "secret")]
client._limiters = {}
client._active_idx = 0
client._timeout_ms = 180000
client._try_load_db_config = lambda: None
attempts = []
async def fake_provider(*_args, **_kwargs):
attempts.append(1)
if len(attempts) == 1:
raise httpx.RemoteProtocolError("incomplete chunked read")
return llm_module.LLMResponse(content='{"ok": true}', provider="deepseek")
async def no_wait(_seconds):
return None
monkeypatch.setattr(llm_module, "_call_provider", fake_provider)
monkeypatch.setattr(llm_module.asyncio, "sleep", no_wait)
response = asyncio.run(client._call([{"role": "user", "content": "test"}], json_mode=True))
assert response.provider == "deepseek"
assert len(attempts) == 2
def test_single_required_array_schema_normalizes_safe_equivalent_roots():
from insurance.ppt.llm_client import _normalize_schema_root
schema = {
"type": "object",
"required": ["benefit_illustration"],
"properties": {"benefit_illustration": {"type": "array"}},
}
assert _normalize_schema_root([{"policy_year": 1}], schema) == {
"benefit_illustration": [{"policy_year": 1}],
}
assert _normalize_schema_root({"rows": [{"policy_year": 2}]}, schema) == {
"benefit_illustration": [{"policy_year": 2}],
}
assert _normalize_schema_root({"message": "not rows"}, schema) == {"message": "not rows"}
assert _normalize_schema_root({"message": []}, schema) == {"message": []}
def test_structured_output_repair_receives_the_missing_field_error():
import asyncio
from types import SimpleNamespace
from insurance.ppt.llm_client import LLMClient
client = LLMClient.__new__(LLMClient)
responses = iter([
SimpleNamespace(content='{"message": "missing root"}'),
SimpleNamespace(content='{"benefit_illustration": []}'),
])
calls = []
async def fake_call(messages, **_kwargs):
calls.append(messages)
return next(responses)
client._call = fake_call
schema = {
"type": "object",
"required": ["benefit_illustration"],
"properties": {"benefit_illustration": {"type": "array"}},
}
parsed, _response = asyncio.run(client.structured_output("extract", schema=schema))
assert parsed == {"benefit_illustration": []}
assert "缺少必填字段: benefit_illustration" in calls[1][-1]["content"]
def test_withdrawal_regex_accepts_title_on_previous_line():
from insurance.ppt.regex_extractor import extract_insurance_regex
data = extract_insurance_regex("""
产品名称:储蓄计划
受保人年龄35
年缴保费10,000
计划提领方案
保单年度 total withdrawn surrender value after
10 50,000 180,000
20 150,000 360,000
""")
rows = data["withdrawal_illustration"]
assert [row["policy_year"] for row in rows] == [10, 20]
assert rows[0]["total_withdrawn"] == 50000
def test_gender_token_is_not_accepted_as_product_name():
from insurance.ppt.extraction import _is_obviously_invalid_product_name
assert _is_obviously_invalid_product_name("Female")
assert _is_obviously_invalid_product_name("")
assert not _is_obviously_invalid_product_name("环盈活储蓄保险计划")
def test_long_poster_fills_partial_milestones_with_real_rows():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"benefit_table": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (1, 10, 11, 12)
],
}, {}, output_mode="long", plan_type="savings")
assert len(content["benefit_table"]) >= 3
assert 10 in [row["year"] for row in content["benefit_table"]]
def test_poster_explicit_milestone_overrides_chart_row():
from insurance.poster.content_builder import build_poster_content
content = build_poster_content({
"surrender_value_10": 99999,
"benefit_table": [
{"policy_year": year, "total_surrender_value": year * 1000}
for year in (10, 20, 30)
],
}, {}, output_mode="long", plan_type="savings")
row_10 = next(row for row in content["benefit_table"] if row["year"] == 10)
assert row_10["totalSurrender"] == 99999
def test_frontend_chart_and_mobile_rules_cover_reported_failures():
root = Path(__file__).resolve().parents[1]
chart = (root / "frontend/src/components/poster/long/PosterBenefitChart.vue").read_text(encoding="utf-8")
ppt_page = (root / "frontend/src/pages/PptPage.vue").read_text(encoding="utf-8")
review = (root / "frontend/src/pages/components/ppt/PptDataReview.vue").read_text(encoding="utf-8")
assert ".once('finished'" not in chart
assert "instance.on('finished'" in chart
assert "instance.off('finished'" in chart
assert "@container" not in ppt_page
assert "@media (max-width: 767px)" in ppt_page
assert 'aria-label="返回 PPT 工作区列表"' in ppt_page
assert "transition: width" not in ppt_page
assert "mobile-data-card" in review
assert "退保价值 · ${getSurrenderRows(currentExt).length} 行" in review
assert ':data="getSurrenderRows(currentExt)"' in review
assert "提领方案" in review
assert "提领/提款方案属于可选情景" in review
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