fix: B9a 复审闭环——P2-1 skipped 分支 has_engine_error_audit 中间态警示(input_summary LIKE)+docstring 局限声明; P2-2 非首笔 trade 幂等用例; P3 顺手: 双脚本连接自检/ImportError 分支/非 dict payload raw 兜底/get_trade_by_id confirmed 过滤/勿并行声明, 8 例 204 绿, warning 路径真库复验后清理; docs 同步复审结论

This commit is contained in:
2026-09-07 01:02:12 +08:00
parent fdf487dc32
commit e62fb1d291
9 changed files with 135 additions and 14 deletions
+61
View File
@@ -24,15 +24,21 @@ from subscribe_alerts import format_alert # noqa: E402
from app.repository.core_ro import CoreReadOnlyRepository
from app.repository.risk_repository import RiskRepository
from app.service.risk import alert_service
from app.service.risk.alert_service import handle_alert
from app.service.risk.engine import process_trade_event
class FakePublisher:
def __init__(self):
self.messages = []
self.deletes = []
def publish(self, channel, payload):
self.messages.append((channel, payload))
def delete(self, *keys):
self.deletes.append(keys)
@pytest.fixture()
def env():
@@ -93,6 +99,7 @@ def test_rebuild_creates_alert_and_publishes(env):
assert alert["status"] == "pending_review" and alert["risk_score"] == 70
(channel, _), = pub.messages
assert channel == "risk:pub:alert"
assert _counts(engine, "audit_log", "decision='alert_created'") == 1 # P-05 留痕链
def test_rebuild_idempotent_skips_second_run(env):
@@ -135,6 +142,59 @@ def test_rebuild_aml_hit_then_idempotent(env):
assert _counts(engine, "risk_alert", "alert_type='aml'") == 1
def test_rebuild_non_first_trade_of_merged_alert_skips(env):
"""B8 复审口径(P2-2):幂等保障依赖 LIKE 对 events[n](n≥1)命中——
同日第二笔 append 进同单后 rebuild,必须 skip 且不重复 append。"""
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB4", "600000")
_seed_trade(conn, "TRD-TEST-RB5", "600000", at=datetime(2026, 9, 6, 15, 0, 0))
first = rebuild_trade("TRD-TEST-RB4", core, repo)
assert first["state"] == "rebuilt"
# 第二笔走正常引擎路径(等价网关同步调用)→ append 进同单
process_trade_event(core.get_trade_by_id("TRD-TEST-RB5"), core_ro=core, risk_repo=repo)
merged_id = first["alert_ids"][0]
assert len(repo.get_alert(merged_id)["payload"]["events"]) == 2
second = rebuild_trade("TRD-TEST-RB5", core, repo)
assert second["state"] == "skipped" and second["alert_ids"] == [merged_id]
assert len(repo.get_alert(merged_id)["payload"]["events"]) == 2 # 不重复 append
def test_rebuild_after_disposal_still_skips(env):
"""已处置单仍 skip(重放不得绕过处置结论,评审 P3-7)。"""
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB6", "600000")
out = rebuild_trade("TRD-TEST-RB6", core, repo)
aid = out["alert_ids"][0]
handle_alert(aid, "confirmed_normal", "STAFF-R1", risk_repo=repo)
again = rebuild_trade("TRD-TEST-RB6", core, repo)
assert again["state"] == "skipped" and again["alert_ids"] == [aid]
assert _counts(engine, "risk_alert") == 1
def test_rebuild_warns_on_engine_error_middle_state(env):
"""P2-1:首张单落库后 engine_error 中断(部分失败中间态)→ skip + warning 提示人工核对。"""
core, repo, pub, engine = env
with engine.begin() as conn:
_seed_trade(conn, "TRD-TEST-RB7", "600000")
out = rebuild_trade("TRD-TEST-RB7", core, repo)
assert "warning" not in out # 正常补偿无警示
# 模拟中断留痕:同一笔再走一遍"交易已成立但引擎异常"的审计(如 aml 单缺失场景)
repo.insert_audit_log(
{
"trace_id": "tr-x", "event_type": "trade_request", "agent_type": "platform",
"actor_id": "SYSTEM", "customer_id": "C1", "rule_id": None,
"input_summary": {"trade_id": "TRD-TEST-RB7", "error_stage": "process_trade_event"},
"decision": "risk_engine_error", "risk_score": None,
"handler_id": None, "handler_result": None, "handler_comment": None,
}
)
again = rebuild_trade("TRD-TEST-RB7", core, repo)
assert again["state"] == "skipped"
assert "人工核对" in again["warning"]
def test_format_alert_renders_payload_fields():
line = format_alert(
{
@@ -150,3 +210,4 @@ def test_format_alert_renders_payload_fields():
"risk_officer,compliance"):
assert frag in line
assert "CUST-9527" not in line # payload 只有脱敏掩码,无原始 id
assert format_alert({"raw": "not-a-json-dict"}) == "[raw] not-a-json-dict"