baodan/tests/plan_snapshot_test.py
2026-08-04 10:44:34 +08:00

307 lines
11 KiB
Python

import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
def test_generate_ppt_does_not_shadow_module_jsonify():
import symtable
route_path = Path(__file__).resolve().parents[1] / "api/insurance/ppt/routes.py"
module = symtable.symtable(route_path.read_text(encoding="utf-8"), str(route_path), "exec")
generate_ppt = next(child for child in module.get_children() if child.get_name() == "generate_ppt")
jsonify = generate_ppt.lookup("jsonify")
assert jsonify.is_global()
assert not jsonify.is_local()
def _evidence(document_id=1, page=2):
return {
"documentId": document_id,
"pageNumber": page,
"bbox": [10, 20, 100, 40],
"tableId": None,
"rowId": None,
"columnId": None,
"textHash": "a" * 64,
}
def _legacy_data(document_id=1):
return {
"product_name": "储蓄计划 A",
"product_type": "savings",
"insured": {"age": 35, "gender": "male", "smoker": "no"},
"policy": {
"currency": "HKD",
"annual_premium": 100000,
"premium_payment_period": 5,
"total_premium": None,
},
"benefit_illustration": [{
"policy_year": 10,
"scenario_type": "base",
"total_surrender_value": 650000,
}],
"meta": {"evidence": [
{"fieldPath": "policy.annual_premium", "evidence": _evidence(document_id, 2)},
{"fieldPath": "benefit.base.10.default.total_surrender_value", "evidence": _evidence(document_id, 8)},
]},
}
def test_candidate_reconciliation_preserves_conflicts_and_context_hint_cannot_override():
from insurance.plan_data.field_values import make_candidate, reconcile_candidates
evidence = [_evidence()]
result = reconcile_candidates([
make_candidate("policy.annualPremium", "100,000", normalized_value=100000, source="pdf_table", evidence=evidence),
make_candidate("policy.annualPremium", 120000, source="llm", confidence=0.99),
make_candidate("identity.productName", "文件名产品", source="context_hint"),
make_candidate("identity.productName", "PDF 产品", source="regex", evidence=evidence),
])
assert result["policy.annualPremium"]["status"] == "conflict"
assert result["policy.annualPremium"]["conflictCandidates"] == [100000, 120000]
assert result["identity.productName"]["status"] == "conflict"
assert set(result["identity.productName"]["conflictCandidates"]) == {"PDF 产品", "文件名产品"}
def test_legacy_conversion_uses_field_values_and_never_derives_contract_total():
from insurance.plan_data.conversion import legacy_to_plan_data
plan_data = legacy_to_plan_data(_legacy_data())
assert plan_data["policy"]["annualPremium"]["value"] == 100000
assert plan_data["policy"]["annualPremium"]["status"] == "extracted"
assert plan_data["policy"]["contractualTotalPremium"]["status"] == "missing"
assert plan_data["policy"]["contractualTotalPremium"]["value"] is None
assert plan_data["benefitScenarios"][0]["rows"][0]["totalSurrenderValue"]["evidence"]
def test_poster_flat_fields_survive_snapshot_conversion():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.snapshot_validation import validate_plan_data
poster_data = {
"product_name": "测试重疾计划",
"plan_type": "ci",
"age": 38,
"gender": "female",
"smoking_status": "no",
"currency": "HKD",
"sum_assured": 1_000_000,
"annual_premium": 25_000,
"premium_term": 20,
"meta": {"evidence": []},
}
plan_data = legacy_to_plan_data(poster_data)
result = validate_plan_data(plan_data, for_confirmation=True)
assert plan_data["identity"]["insuredAge"]["value"] == 38
assert plan_data["identity"]["insuredGender"]["value"] == "female"
assert plan_data["identity"]["insuredSmoker"]["value"] == "no"
assert plan_data["policy"]["currency"]["value"] == "HKD"
assert plan_data["policy"]["sumAssured"]["value"] == 1_000_000
assert plan_data["policy"]["premiumPaymentPeriod"]["value"] == 20
assert result["blockingCount"] == 0
def test_projection_uses_confirmed_table_milestones_and_keeps_unknown_null():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.projections import to_deck_contract, to_poster_projection
plan_data = legacy_to_plan_data(_legacy_data())
poster = to_poster_projection(plan_data)
deck = to_deck_contract(plan_data)
assert poster["surrender_value_10"] == 650000
assert poster["surrender_value_20"] is None
assert poster["total_premium"] is None
assert deck["policy"]["contractualTotalPremium"] is None
assert deck["benefitRows"][0]["totalSurrenderValue"] == 650000
def test_migration_037_and_snapshot_lifecycle_are_versioned_and_owned():
from flask import Flask
from sqlalchemy import inspect
from insurance.db.database import db
from insurance.db.migrate_036 import migrate as migrate_036
from insurance.db.migrate_037 import migrate as migrate_037
from insurance.models.insurance_document import InsuranceDocument
from insurance.plan_data.service import (
SnapshotError,
confirm_snapshot,
create_draft_from_legacy,
patch_draft,
)
app = Flask(__name__)
app.config.update(
SQLALCHEMY_DATABASE_URI="sqlite:///:memory:",
SQLALCHEMY_TRACK_MODIFICATIONS=False,
)
db.init_app(app)
with app.app_context():
migrate_036()
migrate_037()
document = InsuranceDocument(
user_id="user-1",
sha256="b" * 64,
storage_key="/private/plan.pdf",
original_name="plan.pdf",
mime_type="application/pdf",
file_size=100,
page_count=8,
pdf_kind="native",
status="parsed",
parser_version="test-v1",
)
db.session.add(document)
db.session.commit()
legacy = _legacy_data(document.id)
draft = create_draft_from_legacy(document.id, "user-1", legacy)
confirmed = confirm_snapshot(draft.id, "user-1")
assert draft.snapshot_version == 1
assert confirmed.snapshot_version == 2
assert confirmed.previous_snapshot_id == draft.id
assert confirmed.status == "confirmed"
assert confirmed.snapshot_hash != draft.snapshot_hash
assert confirmed.validation()["blockingCount"] == 0
with pytest.raises(SnapshotError) as stale:
patch_draft(
confirmed.id,
"user-1",
expected_hash="wrong",
changes=[{
"fieldPath": "policy.annualPremium",
"value": 110000,
"overrideReason": "客户确认修正",
}],
)
assert stale.value.code == "SNAPSHOT_VERSION_CONFLICT"
edited = patch_draft(
confirmed.id,
"user-1",
expected_hash=confirmed.snapshot_hash,
changes=[{
"fieldPath": "policy.annualPremium",
"value": 110000,
"overrideReason": "客户确认修正",
}],
)
assert edited.snapshot_version == 3
assert edited.plan_data()["policy"]["annualPremium"]["status"] == "confirmed"
assert confirmed.plan_data()["policy"]["annualPremium"]["value"] == 100000
with pytest.raises(SnapshotError):
confirm_snapshot(edited.id, "user-2")
tables = set(inspect(db.engine).get_table_names())
assert {
"insurance_plan_snapshots",
"insurance_field_evidence",
"insurance_plan_overrides",
} <= tables
def test_confirmation_warns_when_critical_amount_evidence_is_missing():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.snapshot_validation import validate_plan_data
data = _legacy_data()
data["meta"]["evidence"] = []
result = validate_plan_data(legacy_to_plan_data(data), for_confirmation=True)
assert result["blockingCount"] == 0
assert result["warningCount"] == 1
assert any(item["code"] == "CRITICAL_EVIDENCE_MISSING" for item in result["issues"])
def test_optional_benefit_and_withdrawal_data_do_not_block_confirmation():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.snapshot_validation import validate_plan_data
data = _legacy_data()
data["meta"]["evidence"] = [
{"fieldPath": "policy.annual_premium", "evidence": _evidence(1, 2)},
]
data["withdrawal_illustration"] = [{
"policy_year": 10,
"annual_withdrawal": 50000,
"surrender_value_after": 300000,
}]
result = validate_plan_data(legacy_to_plan_data(data), for_confirmation=True)
assert result["blockingCount"] == 0
assert not any(
item["code"] == "CRITICAL_EVIDENCE_MISSING"
and (
".benefitScenarios." in item["path"]
or ".withdrawals." in item["path"]
)
for item in result["issues"]
)
def test_missing_optional_projection_sections_do_not_block_confirmation():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.snapshot_validation import validate_plan_data
data = _legacy_data()
data["benefit_illustration"] = []
data["withdrawal_illustration"] = []
result = validate_plan_data(legacy_to_plan_data(data), for_confirmation=True)
assert result["blockingCount"] == 0
def test_confirmation_does_not_treat_source_labels_as_amounts():
from insurance.plan_data.conversion import legacy_to_plan_data
from insurance.plan_data.snapshot_validation import validate_plan_data
data = _legacy_data()
data["policy"]["annual_premium_source_label"] = "IUL利益表保费列"
result = validate_plan_data(legacy_to_plan_data(data), for_confirmation=True)
assert not any(
item["path"].endswith("annualPremiumSourceLabel")
for item in result["issues"]
)
def test_generation_task_exposes_frozen_versions_without_server_paths():
from insurance.models.generation_task import GenerationTask
task = GenerationTask(
id="task-1",
user_id="user-1",
artifact_type="ppt",
operation="generate",
workspace_id="session-1",
input_revision=7,
submit_revision=7,
snapshot_id=3,
snapshot_hash="c" * 64,
renderer_version="renderer-v2",
result_state="stale",
output_json=json.dumps({"filePath": "/private/output.pptx", "downloadUrl": "/download/1"}),
)
payload = task.to_dict()
assert payload["snapshotId"] == 3
assert payload["submitRevision"] == 7
assert payload["rendererVersion"] == "renderer-v2"
assert payload["resultState"] == "stale"
assert payload["output"] == {"downloadUrl": "/download/1"}