"""auto_collect 复核闸门(kpi_review_gate.py)纯函数单测 — 2026-09-14 覆盖:阈值换算(金额/比率)、规则版本指纹、锚点判定、主值 remark 回写幂等与长度安全。 不连库(只构造 KPIValueSource 内存对象)。 """ import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.models import KPIValueSource # noqa: E402 from scripts.kpi_review_gate import ( # noqa: E402 RATIO_PP, _with_recheck_remark, anchor_check, rule_version, tolerance, ) def _mapping(rule, unit=1.0, status="active", kpi_id=1): m = KPIValueSource() m.entity_id = 1 m.kpi_id = kpi_id m.source_table = "voucher_details" m.source_field = "credit_amount" m.aggregate = "sum" m.period_field = "period" m.unit_conversion = unit m.status = status m.filter_rule = rule return m def test_tolerance_amount_class(): # 万元口径(unit=0.0001):max(1000元, 0.5%×主值) → 换算回万元 m = _mapping({"calc": "sum", "subject_codes": ["5001"]}, unit=0.0001) tol, cls = tolerance(m, 81.08) assert cls == "amount" assert abs(tol - 0.4054) < 1e-9 # 0.5%×810800元=4054元=0.4054万 tol_small, _ = tolerance(m, 1.0) # 主值 1 万元 → 0.5%=50元 < 下限 1000元 assert abs(tol_small - 0.1) < 1e-9 def test_tolerance_ratio_class(): m = _mapping({"calc": "ratio", "subject_codes": ["5001", "5401"]}, unit=1.0, kpi_id=2) tol, cls = tolerance(m, 0.05) assert cls == "ratio" and tol == RATIO_PP == 0.005 # 0.34 个百分点在容差内、0.6 个百分点超容差 assert abs(0.3766 - 0.38) <= tol assert abs(0.3860 - 0.38) > tol def test_rule_version_stable_and_changes_on_rule_edit(): r1 = {"calc": "sum", "subject_codes": ["5001"], "validation": {"matched": 3}} r2 = {"calc": "sum", "subject_codes": ["5001"], "validation": {"matched": 2}} v1a = rule_version(_mapping(r1)) v1b = rule_version(_mapping(r1)) assert v1a == v1b # 确定性 + validation 不参与指纹 assert rule_version(_mapping(r2)) == v1a r3 = {"calc": "sum", "subject_codes": ["5001", "6001"], "validation": {"matched": 3}} assert rule_version(_mapping(r3)) != v1a # 科目组合变更 → 版本变 assert rule_version(_mapping({"version": 7, "calc": "sum"})) == "7" def test_anchor_check_period_level(): rule = {"calc": "sum", "validation": {"matched": 2, "ref_periods": ["2026-06", "2026-07", "2026-08"], "checks": [{"period": "2026-06", "match": True}, {"period": "2026-07", "match": False}, {"period": "2026-08", "match": True}]}} m = _mapping(rule, unit=0.0001, kpi_id=3) ok_07, detail_07, agg = anchor_check(m, "2026-07") ok_08, _, _ = anchor_check(m, "2026-08") ok_09, detail_09, _ = anchor_check(m, "2026-09") assert ok_07 is False and "match=False" in detail_07 assert ok_08 is True # 该期锚点通过(规则级 matched=2/3 一并回报) assert agg == {"matched": 2, "anchors": 3} assert ok_09 is False and "无锚点记录" in detail_09 def test_main_remark_idempotent_and_bounded(): seg = ("2026-09-14 voucher_details 复算一致(自动归集id=2128, Δ=0.0000, rule=1 v6eac76a3)" ";主值批次2026-08-24 15:50:01早于源同步2026-09-11 16:35:14(时点差,不改数)") once = _with_recheck_remark(None, seg) twice = _with_recheck_remark(once, seg) assert once == twice, "同一自动行重跑不得重复追加(否则撑爆 varchar(500))" long_old = "旧正文" * 300 # 远超 500 got = _with_recheck_remark(long_old, seg) assert len(got) <= 500 and got.endswith(seg) # 两条不同自动行的留痕都要在(不同 id → 不互相覆盖) seg2 = seg.replace("id=2128", "id=2129") both = _with_recheck_remark(once, seg2) assert "id=2128" in both and "id=2129" in both and len(both) <= 500