Files
cma-management/backend/tests/test_kpi_review_gate_20260914.py
T
Hermes CI Fix 8fa6c7f520 feat(kpi): auto_collect review gate (finance ruling 2026-09-14)
R2 rule-gate auto-promotion + R3 exception interception in the same collect transaction; merge duplicate rows, keep single verified main per (kpi_id, period); reuse kpi_bep_deriver._is_period_closed; no schema change (data_status/remark only).

Task: finance-20260914-autocollect-reviewflow-impl
2026-09-14 18:12:38 +08:00

90 lines
3.9 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.
"""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