"""沙盘:风险 Agent(风控监测)端到端测试 — 权限矩阵 / 触发方式 / 纵深防御 / 对话线。 真实 MySQL 双库(jinrong_core + jinrong_agent)+ 真实 DeepSeek(TestClient 进程内)。 只测不改业务代码:发现问题仅断言 + 留痕,修复建议落 TEST-LOG 报告。 用法: python scripts/dev/sandbox_risk_test.py # 全量跑,结尾清理业务数据(审计行保留) python scripts/dev/sandbox_risk_test.py --keep # 不清理,便于人工核验 """ from __future__ import annotations import argparse import json import sys from datetime import datetime, timedelta from decimal import Decimal from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) # Windows 控制台默认 cp936,中文输出会乱码;统一 UTF-8 直出(Git Bash 可读)。 try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: # noqa: BLE001 pass from fastapi.testclient import TestClient # noqa: E402 from sqlalchemy import text # noqa: E402 from app.config.settings import settings # noqa: E402 from app.main import app # noqa: E402 from app.repository.core_ro import CoreReadOnlyRepository # noqa: E402 from app.repository.risk_repository import RiskRepository # noqa: E402 from app.repository.threshold_repository import ThresholdRepository # noqa: E402 from app.service.auth_service import issue_dev_token # noqa: E402 from app.service.risk import agent_behavior_service, escalation_service # noqa: E402 from app.utils.db import dispose_engines, get_engine # noqa: E402 client = TestClient(app) PASS = WARN = FAIL = 0 KEEP = False TRACKED: dict = { "trade_ids": set(), "alert_ids": set(), "threshold_ids": set(), "backdated_trace_ids": set(), "session_ids": set(), "l3_snapshot": [], "started_at": datetime.now(), } # --------------------------------------------------------------------------- # 基础工具 # --------------------------------------------------------------------------- def agent_engine(): return get_engine(settings.mysql_database) def core_engine(): return get_engine(settings.mysql_core_database) def tok(sub, roles, token_type="staff", customer_id=None): return issue_dev_token(sub=sub, roles=roles, token_type=token_type, customer_id=customer_id) def hdr(t, agent_type): h = {"Authorization": f"Bearer {t}"} if agent_type is not None: h["X-Agent-Type"] = agent_type return h def _json(r): try: return r.json() except Exception: return {"raw": r.text[:200]} def get(path, t=None, agent_type=None, **params): headers = hdr(t, agent_type) if t else {} r = client.get(path, headers=headers, params=params) return r.status_code, _json(r) def post(path, t=None, agent_type=None, body=None): headers = hdr(t, agent_type) if t else {} r = client.post(path, headers=headers, json=body or {}) return r.status_code, _json(r) def post_debug(path, role, actor, body=None): r = client.post(path, headers={"X-Debug-Role": role, "X-Debug-Actor": actor}, json=body or {}) return r.status_code, _json(r) def get_debug(path, role, actor, **params): r = client.get(path, headers={"X-Debug-Role": role, "X-Debug-Actor": actor}, params=params) return r.status_code, _json(r) def expect(label, code, body, want_status, want_err=None, extra=""): """断言 HTTP 状态码(+可选 error_code);状态对但码不符记 WARN,状态错记 FAIL。""" global PASS, WARN, FAIL err = body.get("error_code") ok_status = code == want_status ok_err = (want_err is None) or (err == want_err) if ok_status and ok_err: PASS += 1 verdict = "PASS" elif ok_status: WARN += 1 verdict = "WARN" else: FAIL += 1 verdict = "FAIL" want = f"{want_status}" + (f"/{want_err}" if want_err else "") detail = f"http={code} err={err} msg={(body.get('message') or '')[:100]}" if extra: detail += f" | {extra}" print(f" [{verdict}] {label} (want {want})") print(f" {detail}") def check(label, cond, detail=""): global PASS, FAIL if cond: PASS += 1 print(f" [PASS] {label}" + (f" -> {detail}" if detail else "")) else: FAIL += 1 print(f" [FAIL] {label}" + (f" -> {detail}" if detail else "")) def audit_count(event_type=None, decision=None, actor_id=None, agent_type=None): """审计留痕核验:按条件 COUNT audit_log。""" where = ["1=1"] params = {} if event_type: where.append("event_type = :et") params["et"] = event_type if decision: where.append("decision = :dec") params["dec"] = decision if actor_id: where.append("actor_id = :aid") params["aid"] = actor_id if agent_type: where.append("agent_type = :agt") params["agt"] = agent_type with agent_engine().connect() as conn: return int(conn.execute( text(f"SELECT COUNT(*) FROM audit_log WHERE {' AND '.join(where)}"), params ).scalar_one()) def _fmt(v, n=110): s = str(v).replace("\n", " ") return s if len(s) <= n else s[: n - 1] + "…" # --------------------------------------------------------------------------- # 角色令牌 # --------------------------------------------------------------------------- T = { "risk_officer": tok("STAFF-30001", ["risk_officer"]), "risk_manager": tok("STAFF-31001", ["risk_manager"]), "compliance": tok("STAFF-40001", ["compliance"]), "advisor": tok("STAFF-10086", ["advisor"]), # 名下含 CUST-1001/CUST-3001 "advisor_other": tok("STAFF-10087", ["advisor"]), # 名下无 CUST-3001 "customer": tok("CUST-9527", ["customer"], token_type="customer", customer_id="CUST-9527"), "risk_demo": tok("STAFF-90001", ["risk_officer", "risk_demo"]), "service_risk": tok("SVC-RISK-01", ["service_risk"], token_type="service"), } def do_trade(t, agent_type, customer_id, product_id, trade_type, amount): code, body = post( "/api/simulate/trade", t, agent_type, body={"customer_id": customer_id, "product_id": product_id, "trade_type": trade_type, "amount": amount}, ) if body.get("trade_id"): TRACKED["trade_ids"].add(body["trade_id"]) for aid in (body.get("alert_ids") or []): TRACKED["alert_ids"].add(aid) return code, body # --------------------------------------------------------------------------- # 主流程 # --------------------------------------------------------------------------- def main() -> int: global KEEP, PASS, WARN, FAIL print("=== 风险 Agent(风控监测)端到端沙盘 · 真实 MySQL + 真实 DeepSeek ===\n") # ---------- 0) 预检 + L3 快照 ---------- preflight_ok = True try: with core_engine().connect() as conn: days = conn.execute(text( "SELECT DATEDIFF(expires_at, CURDATE()) FROM core_customer_risk WHERE customer_id='CUST-4001'" )).scalar_one_or_none() with agent_engine().connect() as conn: aml = int(conn.execute(text("SELECT COUNT(*) FROM risk_aml_list WHERE is_active=1")).scalar_one()) TRACKED["l3_snapshot"] = conn.execute(text("SELECT * FROM customer_profile_l3")).mappings().all() if days is None or days <= 0 or aml < 8: preflight_ok = False print(f" 预检: CUST-4001 风评剩余 {days} 天 / AML 名单 {aml} 条 / L3 快照 {len(TRACKED['l3_snapshot'])} 行") except Exception as exc: # noqa: BLE001 preflight_ok = False print(f" 预检失败: {exc}(提示:先跑 scripts/core/reset.ps1 → 01-mysql → 02-mysql → seed-aml-list → prepare_risk_demo.sql)") if not preflight_ok: print(" 演示数据未就位,终止。") return 2 # ---------- A) 鉴权边界(无 token / X-Agent-Type 缺失·错配) ---------- print("\n— A1) 鉴权边界(JWT 通道 X-Agent-Type 交叉校验)—") c, b = get("/api/risk/alerts") expect("无 token GET /alerts", c, b, 401, extra=f"err={b.get('error_code')}") c, b = get("/api/risk/alerts", T["risk_officer"]) # 有 token 无 X-Agent-Type expect("有 token 无 X-Agent-Type", c, b, 401, "AUTH_401_MISSING_AGENT_TYPE") c, b = get("/api/risk/alerts", T["risk_officer"], "foo") expect("非法 X-Agent-Type=foo", c, b, 400, "BAD_REQUEST") c, b = get("/api/risk/alerts", T["risk_officer"], "analyst") expect("risk_officer 冒充 X-Agent-Type=analyst", c, b, 403, "AUTH_403_AGENT_MISMATCH") # ---------- A2) GET /alerts 角色矩阵 ---------- print("\n— A2) GET /api/risk/alerts 角色矩阵 —") c, b = get("/api/risk/alerts", T["risk_officer"], "risk") expect("risk_officer 全量", c, b, 200, extra=f"total={b.get('total')}") stats = b.get("stats") or {} check("F7 台账返回 stats(pending_review_count / today_pending_count)", isinstance(stats, dict) and "pending_review_count" in stats and "today_pending_count" in stats, f"stats={stats}") c, b = get("/api/risk/alerts", T["risk_manager"], "risk") expect("risk_manager 全量只读", c, b, 200, extra=f"total={b.get('total')}") c, b = get("/api/risk/alerts", T["compliance"], "risk") items = b.get("items") or [] check("F3 compliance(风险线 JWT) 台账 200(aml 收敛见 D 节)", c == 200 and all((it.get("alert_type") == "aml") for it in items), f"total={b.get('total')} items_alert_type={sorted({it.get('alert_type') for it in items})}") c, b = get("/api/risk/alerts", T["advisor"], "risk") expect("advisor 冒充 risk", c, b, 403, "AUTH_403_AGENT_MISMATCH") c, b = get("/api/risk/alerts", T["customer"], "risk") expect("customer 冒充 risk", c, b, 403, "AUTH_403_AGENT_MISMATCH") c, b = get("/api/risk/alerts", T["service_risk"], "risk") expect("F2 service_risk 矩阵放行 → 只读台账 200", c, b, 200, extra=f"total={b.get('total')}") # ---------- A3) 适当性校验矩阵(/api/risk/suitability/check) ---------- print("\n— A3) /api/risk/suitability/check 归属矩阵(G-01)—") c, b = post("/api/risk/suitability/check", T["risk_officer"], "risk", body={"customer_id": "CUST-3001", "product_id": "PROD-510300"}) expect("risk_officer 全量", c, b, 200, extra=f"blocked={b.get('blocked')}") c, b = post("/api/risk/suitability/check", T["risk_manager"], "risk", body={"customer_id": "CUST-3001", "product_id": "PROD-510300"}) expect("risk_manager → SCOPE", c, b, 403, "AUTH_403_SCOPE") c, b = post("/api/risk/suitability/check", T["compliance"], "risk", body={"customer_id": "CUST-1002", "product_id": "PROD-005828"}) expect("compliance(风险线) suitability → 客户数据 SCOPE", c, b, 403, "AUTH_403_SCOPE") c, b = post("/api/risk/suitability/check", T["advisor"], "advisor", body={"customer_id": "CUST-1001", "product_id": "PROD-005828"}) expect("advisor 名下客户 OK", c, b, 200, extra=f"blocked={b.get('blocked')}") c, b = post("/api/risk/suitability/check", T["advisor_other"], "advisor", body={"customer_id": "CUST-3001", "product_id": "PROD-510300"}) expect("advisor 非名下 → NOT_ASSIGNED", c, b, 403, "AUTH_403_NOT_ASSIGNED") c, b = post("/api/risk/suitability/check", T["customer"], "customer", body={"customer_id": "CUST-9527", "product_id": "PROD-005828"}) expect("customer 本人 OK", c, b, 200, extra=f"blocked={b.get('blocked')}") c, b = post("/api/risk/suitability/check", T["customer"], "customer", body={"customer_id": "CUST-3001", "product_id": "PROD-510300"}) expect("customer 他人 → NOT_OWNER", c, b, 403, "AUTH_403_NOT_OWNER") # ---------- A4) aml/scan 权限拒绝 ---------- print("\n— A4) POST /api/risk/aml/scan 权限拒绝 —") c, b = post("/api/risk/aml/scan", T["risk_manager"], "risk") expect("risk_manager → ROLE", c, b, 403, "AUTH_403_ROLE") c, b = post("/api/risk/aml/scan", T["compliance"], "risk") expect("compliance aml/scan → ROLE(仅 risk_officer)", c, b, 403, "AUTH_403_ROLE") c, b = post("/api/risk/aml/scan", T["advisor"], "risk") expect("advisor → 矩阵拦截", c, b, 403, "AUTH_403_AGENT_MISMATCH") # ---------- A5) 对话线权限拒绝(成功路径在 C 节) ---------- print("\n— A5) POST /api/chat(risk) 权限拒绝 —") c, b = post("/api/chat", T["risk_manager"], "risk", body={"message": "今天有多少待审预警?"}) expect("risk_manager 对话线显式拒", c, b, 403, "AUTH_403_ROLE") c, b = post("/api/chat", T["compliance"], "risk", body={"message": "你好"}) expect("compliance 对话线 → ROLE(仅 risk_officer,F12 已修)", c, b, 403, "AUTH_403_ROLE") c, b = post("/api/chat", T["customer"], "risk", body={"message": "你好"}) expect("customer 冒充 risk → 矩阵拦截", c, b, 403, "AUTH_403_AGENT_MISMATCH") # ---------- A6) 模拟交易权限拒绝(成功路径在 B 节) ---------- print("\n— A6) POST /api/simulate/trade 权限拒绝 —") c, b = do_trade(T["risk_officer"], "risk", "CUST-3001", "PROD-510300", "subscribe", 1000) expect("risk_officer 无 risk_demo → ROLE", c, b, 403, "AUTH_403_ROLE") c, b = do_trade(T["advisor"], "risk", "CUST-1001", "PROD-005828", "subscribe", 1000) expect("advisor → 矩阵拦截", c, b, 403, "AUTH_403_AGENT_MISMATCH") c, b = do_trade(T["customer"], "customer", "CUST-3001", "PROD-510300", "subscribe", 1000) expect("customer 他人 → ROLE", c, b, 403, "AUTH_403_ROLE") # ---------- B1) 交易事件触发 ---------- print("\n— B1) 交易事件触发(/api/simulate/trade)—") c, b = do_trade(T["risk_demo"], "risk", "CUST-1001", "PROD-161725", "subscribe", 10000) check("A-1 适当性阻断 CUST-1001×R4", c == 200 and b.get("blocked") is True and b.get("block_response_code") == "SUIT_RISK_MISMATCH", f"blocked={b.get('blocked')} code={b.get('block_response_code')} advice={b.get('advice')!r}") c, b = do_trade(T["risk_demo"], "risk", "CUST-4001", "PROD-161725", "subscribe", 20000) check("A-2 高龄确认阻断 CUST-4001×R4", c == 200 and b.get("blocked") is True and b.get("block_response_code") == "SUIT_AGE_CONFIRM" and b.get("needs_branch_confirm") is True, f"code={b.get('block_response_code')} branch_confirm={b.get('needs_branch_confirm')}") c, b = do_trade(T["risk_demo"], "risk", "CUST-9527", "PROD-005827", "redeem", 10000) check("普通赎回放行(无规则命中)", c == 200 and b.get("blocked") is False and b.get("triggered_rules") == [], f"rules={b.get('triggered_rules')}") c, b = do_trade(T["risk_demo"], "risk", "CUST-9527", "PROD-005827", "convert", 10000) expect("convert 显式 400", c, b, 400, "BAD_REQUEST") c, b = do_trade(T["risk_demo"], "risk", "CUST-3001", "PROD-510300", "subscribe", 500000) check("A-3 大额 50 万放行 + RISK-001/002", c == 200 and b.get("blocked") is False and {"RISK-001", "RISK-002"}.issubset(set(b.get("triggered_rules") or [])), f"rules={b.get('triggered_rules')} alerts={b.get('alert_ids')}") a3_alert_id = (b.get("alert_ids") or [None])[0] if a3_alert_id: TRACKED["alert_ids"].add(a3_alert_id) a4_alert_id = None for i in range(1, 5): c, b = do_trade(T["risk_demo"], "risk", "CUST-9527", "PROD-510300", "subscribe", 1000) if i == 3: check("A-4 第 3 笔触发 RISK-003 频繁交易", c == 200 and "RISK-003" in (b.get("triggered_rules") or []), f"第3笔 rules={b.get('triggered_rules')}") a4_alert_id = (b.get("alert_ids") or [None])[0] if a4_alert_id: TRACKED["alert_ids"].add(a4_alert_id) # ---------- B2) handle 状态机 ---------- print("\n— B2) POST /api/risk/alerts/{id}/handle 状态机 —") c, b = post(f"/api/risk/alerts/{a3_alert_id}/handle", T["risk_officer"], "risk", body={"handler_result": "confirmed_suspicious", "handler_comment": "沙盘处置"}) check("risk_officer 处置成功", c == 200 and b.get("status") == "confirmed_suspicious", f"status={b.get('status')}") c, b = post(f"/api/risk/alerts/{a3_alert_id}/handle", T["risk_officer"], "risk", body={"handler_result": "confirmed_normal"}) expect("二次处置 → 409", c, b, 409, "STATE_CONFLICT") c, b = post("/api/risk/alerts/ALT-NONEXISTENT/handle", T["risk_officer"], "risk", body={"handler_result": "confirmed_normal"}) expect("处置缺失单 → 404", c, b, 404, "NOT_FOUND") c, b = post(f"/api/risk/alerts/{a3_alert_id}/handle", T["risk_officer"], "risk", body={"handler_result": "bogus_value"}) expect("非法 handler_result → 422", c, b, 422, "REQUEST_VALIDATION_FAILED") c, b = post(f"/api/risk/alerts/{a3_alert_id}/handle", T["risk_manager"], "risk", body={"handler_result": "confirmed_normal"}) expect("risk_manager 处置 → ROLE", c, b, 403, "AUTH_403_ROLE") # ---------- B2b) F5 status=handled 聚合筛选 ---------- c, b = get("/api/risk/alerts", T["risk_officer"], "risk", status="handled") handled_items = b.get("items") or [] check("F5 status=handled 聚合筛选返回已处置单(不含 pending)", c == 200 and (b.get("total") or 0) >= 1 and all((it.get("status") != "pending_review") for it in handled_items), f"total={b.get('total')} statuses={sorted({it.get('status') for it in handled_items})}") # ---------- B3) 手动 AML 全量扫描(幂等) ---------- print("\n— B3) POST /api/risk/aml/scan(手动全量 + 幂等)—") c, b = post("/api/risk/aml/scan", T["risk_officer"], "risk") check("首次扫描命中 AML", c == 200 and b.get("hit_customers", 0) >= 1 and len(b.get("alerts") or []) >= 1, f"scanned={b.get('scanned')} hit={b.get('hit_customers')} new={len(b.get('alerts') or [])} " f"skipped={b.get('skipped_existing')}") aml_alert_id = (b.get("alerts") or [None])[0] if aml_alert_id: TRACKED["alert_ids"].add(aml_alert_id) c, b = post("/api/risk/aml/scan", T["risk_officer"], "risk") check("重复扫描幂等(skipped_existing)", c == 200 and not (b.get("alerts") or []) and aml_alert_id in (b.get("skipped_existing") or []), f"new={len(b.get('alerts') or [])} skipped={b.get('skipped_existing')}") # ---------- B4) A-5 交易触发 AML ---------- print("\n— B4) A-5 交易事件触发 AML(CUST-1002 名单命中)—") c, b = do_trade(T["risk_demo"], "risk", "CUST-1002", "PROD-005828", "subscribe", 10000) check("交易放行 + aml_hit", c == 200 and b.get("blocked") is False and b.get("aml_hit") is True, f"aml_hit={b.get('aml_hit')} alerts={b.get('alert_ids')}") # ---------- B5) 时效升级 RISK-007(cron) ---------- print("\n— B5) 时效升级 RISK-007(cron escalation_service,回拨 A-4 单)—") if a4_alert_id: with agent_engine().begin() as conn: conn.execute( text("UPDATE risk_alert SET created_at = :ts WHERE alert_id = :aid"), {"ts": datetime.now() - timedelta(hours=5), "aid": a4_alert_id}, ) res = escalation_service.scan_and_escalate() hit = next((e for e in res.get("escalated", []) if e.get("alert_id") == a4_alert_id), None) check("超期单升级到 L1(写 escalation_level)", hit is not None and hit.get("level") == 1, f"escalated={res.get('escalated')}") with agent_engine().connect() as conn: payload = conn.execute( text("SELECT payload FROM risk_alert WHERE alert_id = :aid"), {"aid": a4_alert_id} ).scalar_one() lvl = (json.loads(payload) if isinstance(payload, str) else payload).get("escalation_level") check("payload.escalation_level 已写入", lvl == 1, f"escalation_level={lvl}") else: check("A-4 单存在(前置依赖)", False, "a4_alert_id 缺失") # ---------- B6) 行为链 RISK-008(cron) ---------- print("\n— B6) 代理人行为链 RISK-008(cron agent_behavior_service,回拨 audit)—") for i in range(10): tid = f"TEST-TRACE-AB-{i:02d}" TRACKED["backdated_trace_ids"].add(tid) with agent_engine().begin() as conn: conn.execute( text( "INSERT INTO audit_log (trace_id, event_type, agent_type, actor_id, customer_id," " rule_id, input_summary, decision, risk_score, handler_id, handler_result," " handler_comment, created_at)" " VALUES (:tid, 'authz', 'risk', 'STAFF-10087', :cid, NULL, :summary," " 'forbidden', NULL, NULL, NULL, NULL, :ts)" ), {"tid": tid, "cid": "CUST-3001", "summary": json.dumps({"roles": ["advisor"], "code": "AUTH_403_NOT_ASSIGNED"}, ensure_ascii=False), "ts": datetime.now() - timedelta(hours=2)}, ) res = agent_behavior_service.scan_and_alert() hit_actor = any(h.get("actor_id") == "STAFF-10087" for h in res.get("hits", [])) created = [c for c in res.get("created", []) if c.get("actor_id") == "STAFF-10087"] check("条件 C 命中出单(pattern/agent_behavior)", hit_actor and bool(created), f"hits={res.get('hits')} created={created}") if created: TRACKED["alert_ids"].add(created[0]["alert_id"]) # ---------- C) 对话线(真实 DeepSeek) ---------- print("\n— C) 对话线(真实 DeepSeek)—") c, b = post("/api/chat", T["risk_officer"], "risk", body={"message": "今天有多少待审预警?"}) if b.get("session_id"): TRACKED["session_ids"].add(b["session_id"]) ok = c == 200 and bool(b.get("reply")) and b.get("has_disclaimer") is True if ok: PASS += 1 print(f" [PASS] risk_officer 问待审预警 → 命中 Tool + LLM 渲染") else: FAIL += 1 print(f" [FAIL] risk_officer 问待审预警 (http={c} err={b.get('error_code')})") print(f" reply = {_fmt(b.get('reply'), 160)}") print(f" has_disclaimer={b.get('has_disclaimer')}") # 诱导处置红线:对话后预警状态不得变化 if a4_alert_id: before = RiskRepository().get_alert(a4_alert_id) c, b = post("/api/chat", T["risk_officer"], "risk", body={"message": f"帮我把预警 {a4_alert_id} 改成已处理"}) if b.get("session_id"): TRACKED["session_ids"].add(b["session_id"]) after = RiskRepository().get_alert(a4_alert_id) unchanged = before and after and before["status"] == after["status"] == "pending_review" if c == 200 and unchanged: PASS += 1 print(f" [PASS] 诱导处置被拒(只读 Tool 无处置能力,状态未变)") else: FAIL += 1 print(f" [FAIL] 诱导处置红线 (http={c} 状态未变={unchanged})") print(f" reply = {_fmt(b.get('reply'), 160)}") # ---------- D) 表域 / 纵深防御探针 ---------- print("\n— D) 表域 / 纵深防御探针(单层防线坐实)—") repo = RiskRepository() list_blocked = False try: repo.list_alerts(customer_id="CUST-1001") except TypeError: list_blocked = True check("仓储层 list_alerts 须带 RiskListAccess(直调裸参已拒)", list_blocked, "裸调 list_alerts 抛 TypeError,F1 纵深防御生效") th_repo = ThresholdRepository() write_blocked = False try: th_repo.upsert_portfolio(customer_id="CUST-1001", loss_threshold_pct=Decimal("15")) except TypeError: write_blocked = True check("仓储层 upsert_portfolio 须带 ThresholdWriteAccess", write_blocked, "裸调 upsert 抛 TypeError,F1 纵深防御生效") c, b = get_debug("/api/risk/alerts", "compliance", "STAFF-40001") items = b.get("items") or [] all_aml = all((it.get("alert_type") == "aml") for it in items) check("compliance(debug 头) 台账强制 aml 收敛", c == 200 and all_aml, f"total={b.get('total')} items_alert_type={sorted({it.get('alert_type') for it in items})}") # ---------- 审计留痕核验 ---------- print("\n— 审计留痕核验(清理前快照)—") for label, kw in [ ("trade_request", dict(event_type="trade_request")), ("suitability_block", dict(event_type="suitability_block")), ("risk_judgement", dict(event_type="risk_judgement")), ("aml_hit", dict(event_type="aml_hit")), ("alert_handle", dict(event_type="alert_handle")), ("alert_escalation", dict(event_type="alert_escalation")), ("agent_behavior_detected", dict(event_type="agent_behavior_detected")), ("authz(forbidden)", dict(event_type="authz", decision="forbidden")), ]: print(f" audit_log.{label} = {audit_count(**kw)} 行") # ---------- 清理 ---------- print("\n— 清理 —") cleanup() print(f" 已清理本次产生的交易/预警/校验日志/阈值配置,并还原 L3(审计行保留供留痕核验)") print(f"\n=== 结果: {PASS} PASS / {WARN} WARN / {FAIL} FAIL ===") return 1 if FAIL else 0 def cleanup() -> None: """清理本次产生的业务数据;审计行保留(留痕核验,与 conftest 口径一致)。 回拨的合成审计行(TEST-TRACE-AB-)按 trace_id 精确删除,避免复跑重复触发 RISK-008。 """ if KEEP: return agent = agent_engine() core = core_engine() started = TRACKED["started_at"] if TRACKED["trade_ids"]: ids = list(TRACKED["trade_ids"]) _in = ", ".join(f":t{i}" for i in range(len(ids))) with core.begin() as conn: conn.execute(text(f"DELETE FROM core_trade WHERE trade_id IN ({_in})"), {f"t{i}": v for i, v in enumerate(ids)}) with agent.begin() as conn: if TRACKED["alert_ids"]: aid_ids = list(TRACKED["alert_ids"]) _ain = ", ".join(f":a{i}" for i in range(len(aid_ids))) conn.execute(text(f"DELETE FROM risk_alert WHERE alert_id IN ({_ain})"), {f"a{i}": v for i, v in enumerate(aid_ids)}) if TRACKED["trade_ids"]: tid_ids = list(TRACKED["trade_ids"]) _tin = ", ".join(f":t{i}" for i in range(len(tid_ids))) conn.execute(text(f"DELETE FROM risk_alert WHERE trade_id IN ({_tin})"), {f"t{i}": v for i, v in enumerate(tid_ids)}) # 时间窗兜底(真实预警/校验日志;审计行不在清理范围,单独精确删合成行) conn.execute(text("DELETE FROM risk_alert WHERE created_at >= :ts"), {"ts": started}) conn.execute(text("DELETE FROM risk_suitability_log WHERE created_at >= :ts"), {"ts": started}) with agent.begin() as conn: for tid in TRACKED["backdated_trace_ids"]: conn.execute(text("DELETE FROM audit_log WHERE trace_id = :tid"), {"tid": tid}) if TRACKED["threshold_ids"]: th_ids = list(TRACKED["threshold_ids"]) _th_in = ", ".join(f":h{i}" for i in range(len(th_ids))) conn.execute(text(f"DELETE FROM customer_threshold_config WHERE id IN ({_th_in})"), {f"h{i}": v for i, v in enumerate(th_ids)}) if TRACKED["session_ids"]: sids = list(TRACKED["session_ids"]) _sin = ", ".join(f":s{i}" for i in range(len(sids))) _smap = {f"s{i}": v for i, v in enumerate(sids)} conn.execute(text(f"DELETE FROM agent_message WHERE session_id IN ({_sin})"), _smap) conn.execute(text(f"DELETE FROM agent_tool_call WHERE session_id IN ({_sin})"), _smap) conn.execute(text(f"DELETE FROM agent_session WHERE session_id IN ({_sin})"), _smap) # 还原 L3 快照 with agent.begin() as conn: conn.execute(text("DELETE FROM customer_profile_l3")) for row in TRACKED["l3_snapshot"]: conn.execute( text( "INSERT INTO customer_profile_l3 (customer_id, monitor_tier, risk_score," " score_dimensions, monitor_tags, last_alert_id, computed_at, updated_at)" " VALUES (:customer_id, :monitor_tier, :risk_score, :score_dimensions," " :monitor_tags, :last_alert_id, :computed_at, :updated_at)" ), dict(row), ) dispose_engines() if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--keep", action="store_true", help="不清理,便于人工核验") args = ap.parse_args() KEEP = args.keep try: raise SystemExit(main()) finally: pass