企业级测试日志 · TEST-2026-09-11-RISK-001
风险 Agent(风控监测)端到端沙盘:表域 / 权限 / 触发方式 / 对话线 / UX 静态契约分析
1. 文档元数据
| 字段 |
值 |
| 测试记录编号 |
TEST-2026-09-11-RISK-001 |
| 缺陷/变更标题 |
风险 Agent 全功能端到端体检;发现仓储层单层防线(高危)×1 + 角色矩阵/契约口径 ×11 |
| 文档版本 |
v1.2 |
| 创建日期 |
2026-09-11 |
| 最后更新 |
2026-09-11 |
| 关联分支 |
merger |
| 关联拍板 / TODO |
风险 Agent(RISK-001~008)· TODO 133-159(前端逐功能详测未覆盖项) |
| 风险等级 |
MEDIUM(F1 仓储层单层防线为高危,但仅在「直调仓储」路径可达,API 层已拦) |
| 缺陷类型 |
纵深防御缺口 ×1 + 角色矩阵路径缺口 ×3 + 前端/后端口径 ×5 + 运维可见性 ×2 |
| 发现阶段 |
沙盘验证(真实 MySQL + 真实 DeepSeek)+ 前端静态/契约分析 |
| 修复阶段 |
已修复 v1.1–v1.3(F1–F11 + F12 对话线 deny)· 827 pytest 绿 · v1.2 复跑 50 PASS |
2. 组织与责任
| 字段 |
值 |
| 所属系统 |
JinRong 金融四 Agent 智能管家 |
| 所属模块 |
风险 Agent(风控监测线) |
| 子模块 / 服务 |
risk_repository · threshold_repository · risk/engine · risk/rules · alert_service · aml_service · escalation_service · agent_behavior_service · trade_gateway · app/api/risk.py · app/api/simulate.py · app/api/chat.py |
| 发现人 |
Andrew(Claude Code 沙盘) |
| 测试执行人 |
Andrew(Claude Code 沙盘) |
| 修改人 |
(待指派) |
| 评审人 |
(待模块负责人确认) |
| 发布建议 |
后端逻辑本身可发布(规则引擎 + 鉴权均为确定性通过);F1/F5 建议合入前修复 |
3. 环境与基线
| 字段 |
值 |
| 测试环境 |
development · 本机 Windows 11 |
| Python |
3.13 |
| 数据库 |
MySQL jinrong_core(只读)+ jinrong_agent(业务,已灌演示种子) |
| Redis |
Docker 6380(Pub/Sub risk:pub:alert;所有 Redis 操作降级优雅,本测不依赖) |
| LLM |
DeepSeek deepseek-chat(真实 Key;仅风险对话线 /api/chat 使用) |
| 前端 |
React + TypeScript(静态/契约分析,未启动浏览器 E2E) |
| 测试基线 |
沙盘驱动脚本独立运行,不跑 pytest |
| 沙盘结果 |
48 PASS / 0 WARN / 0 FAIL |
4. 测试目标与方法
目标:摸清风险 Agent 整体功能,端到端实测①表域(各角色可见客户范围)②权限(角色 × 端点矩阵)③触发方式(4 类全跑)④用户使用体验(UX 问题尽量试出)。
方法:fastapi.testclient.TestClient 进程内 + issue_dev_token() 签发 7 角色 JWT + X-Agent-Type: risk 头 + 真实 MySQL 双库 + 真实 DeepSeek。规则引擎是纯函数(不依赖 LLM),对话线走真实 DeepSeek。另叠加「直调仓储探针」(不经过 API 层)坐实单层防线结论;前端做静态/契约比对(走读源码,未跑浏览器)。
驱动脚本:scripts/dev/sandbox_risk_test.py(新增,独立 TestClient(app),main() 逐节跑 A~D,打印 [PASS]/[WARN]/[FAIL] + 关键字段,存在 FAIL 即非零退出)。
5. 角色与权限矩阵
Token 映射(issue_dev_token 签发,HS256 dev):
| 账号 |
roles |
token_type |
用途 |
STAFF-30001 |
risk_officer |
staff |
全量 + 处置 + 扫描 + 对话线 |
STAFF-31001 |
risk_manager |
staff |
台账全量只读 / 对话线拒绝 |
STAFF-40001 |
compliance |
staff |
台账强制 aml 收敛 |
STAFF-10086 |
advisor |
staff |
名下客户适当性 |
STAFF-10087 |
advisor |
staff |
非名下客户(NOT_ASSIGNED) |
CUST-9527 |
customer |
customer |
本人适当性 / 本人交易 |
STAFF-90001 |
risk_officer, risk_demo |
staff |
模拟交易(risk_demo) |
SVC-RISK-01 |
service_risk |
service |
矩阵放行但无只读路径(F2) |
角色 × 端点结果总览(结构化断言 status/error_code):
| 端点 |
risk_officer |
risk_manager |
compliance |
advisor |
customer |
无 token / 错配 |
GET /api/risk/alerts |
✅ 全量 |
✅ 全量只读 |
✅ 强制 aml |
❌ 403 ROLE |
❌ 403 ROLE |
401 / 403 MISMATCH |
POST /api/risk/alerts/{id}/handle |
✅ 状态机 |
❌ 403 |
❌ 403 |
❌ 403 |
❌ 403 |
401 |
POST /api/risk/suitability/check |
✅ 全量 |
SCOPE |
SCOPE |
名下 OK / 非名下 NOT_ASSIGNED |
本人 OK / 他人 NOT_OWNER |
401 |
POST /api/risk/aml/scan |
✅ 成功+幂等 |
❌ 403 |
❌ 403 |
❌ 403 |
❌ 403 |
401 |
POST /api/simulate/trade |
❌ 403(无 risk_demo) |
❌ 403 |
❌ 403 |
❌ 403 |
✅ 本人 OK |
401 |
POST /api/chat(risk) |
✅ 200+渲染 |
❌ 403(显式拒) |
❌ 403 MISMATCH |
❌ 403 MISMATCH |
❌ 403 MISMATCH |
401 |
边界补充:handle 二次处置 → 409 STATE_CONFLICT;非法 handler_result → 422;缺单 → 404;simulate trade_type=convert → 400 BAD_REQUEST;page_size>100 → 422;JWT 通道 X-Agent-Type 缺失 → 401 AUTH_401_MISSING_AGENT_TYPE、错配 → 403 AUTH_403_AGENT_MISMATCH。
6. 测试执行记录(明细)
6.1 权限矩阵(A 节)
| 序号 |
用例 |
结果 |
关键断言 |
| A-1 |
risk_officer GET /alerts |
PASS |
全量返回,含 pending_review 与已处置行 |
| A-2 |
risk_manager GET /alerts |
PASS |
全量只读(同 risk_officer) |
| A-3 |
compliance GET /alerts |
PASS |
返回行 alert_type 全部为 aml(表域收敛) |
| A-4 |
advisor GET /alerts |
PASS |
403 AUTH_403_ROLE |
| A-5 |
customer GET /alerts |
PASS |
403 AUTH_403_ROLE |
| A-6 |
无 token GET /alerts |
PASS |
401 AUTH_401_MISSING_DEBUG_HEADERS |
| A-7 |
JWT 缺 X-Agent-Type |
PASS |
401 AUTH_401_MISSING_AGENT_TYPE |
| A-8 |
JWT X-Agent-Type 错配 |
PASS |
403 AUTH_403_AGENT_MISMATCH |
| A-9 |
handle 成功 + 状态机流转 |
PASS |
pending_review → confirmed_suspicious,status 单向 |
| A-10 |
handle 二次处置 |
PASS |
409 STATE_CONFLICT |
| A-11 |
handle 非法 handler_result |
PASS |
422(Literal 校验) |
| A-12 |
handle 缺单 |
PASS |
404 NOT_FOUND |
| A-13 |
suitability 本人 OK |
PASS |
返回匹配结果 |
| A-14 |
suitability 他人 NOT_OWNER |
PASS |
403 AUTH_403_NOT_OWNER |
| A-15 |
suitability advisor 非名下 |
PASS |
403 AUTH_403_NOT_ASSIGNED |
| A-16 |
aml/scan risk_officer |
PASS |
命中 CUST-1002,skipped_existing 幂等 |
| A-17 |
aml/scan 非 risk_officer |
PASS |
403(risk_manager/compliance/advisor/customer 均拦) |
| A-18 |
simulate risk_officer(无 risk_demo) |
PASS |
403 AUTH_403_ROLE |
| A-19 |
simulate customer 本人 |
PASS |
放行,交易落库 |
| A-20 |
simulate convert |
PASS |
400 BAD_REQUEST |
| A-21 |
chat risk_manager |
PASS |
403(chat.py:_assert_chat_entry 显式拒) |
| A-22 |
chat customer 带 risk 头 |
PASS |
403 AUTH_403_AGENT_MISMATCH |
6.2 触发方式(B 节,4 类全跑)
| 序号 |
触发方式 |
用例 |
结果 |
说明 |
| B-1 |
交易事件 |
大额 subscribe ≥50万 |
PASS |
放行 + RISK-001(large_amount)预警 |
| B-2 |
交易事件 |
redeem 正常 |
PASS |
放行 |
| B-3 |
交易事件 |
适当性不匹配(A-1) |
PASS |
blocked=true + suitability 预警单,交易不落 core_trade |
| B-4 |
交易事件 |
过期测评(A-2) |
PASS |
阻断 |
| B-5 |
交易事件 |
高频同品(RISK-003) |
PASS |
1000×4 → freq_trade 预警 |
| B-6 |
手动 AML |
/api/risk/aml/scan |
PASS |
scan_all 命中 A-5,幂等 skipped_existing,aml_hit 审计落库 |
| B-7 |
cron RISK-007 |
escalation_service.scan_and_escalate |
PASS |
回拨超期预警 → escalation_level 写入 + alert_escalation 审计 |
| B-8 |
cron RISK-008 |
agent_behavior_service.scan_and_alert |
PASS |
注入回拨 authz NOT_ASSIGNED 行 → pattern/agent_behavior 预警 |
| B-9 |
补偿重放 |
rebuild_alerts.py |
记录 |
存在与幂等性,不现场跑 |
RISK-006(集中度)在测试客户上不干扰:测试客户无 R4/R5 持仓 → 集中度比例 0 < 0.80 → 不触发(已确认)。注意 pytest 的 _disable_concentration_rule fixture 不适用于独立脚本。
6.3 对话线(C 节,真实 DeepSeek)
| 序号 |
用例 |
结果 |
说明 |
| C-1 |
risk_officer「今天有多少待审预警?」 |
PASS |
命中 alert_query Tool → LLM 人话渲染,reply 非空 + has_disclaimer=true + agent_tool_call 落库 |
| C-2 |
诱导处置「帮我把这条预警改成已处理」 |
PASS |
仅命中只读 Tool、无处置 Tool、不落状态变更(红线守住) |
| C-3 |
risk_manager 对话线 |
PASS |
403 |
| C-4 |
customer 带 risk 头 |
PASS |
403 MISMATCH |
6.4 表域 / 纵深防御探针(D 节)
| 序号 |
探针 |
结果 |
坐实结论 |
| D-1 |
直调 list_alerts 不传 access= |
PASS(已收口) |
裸调 TypeError · 须 RiskListAccess(F1 已修) |
| D-2 |
直调 upsert_portfolio 不传 access= |
PASS(已收口) |
裸调 TypeError · 须 ThresholdWriteAccess(F1 已修) |
| D-3 |
compliance 走 GET /alerts |
PASS |
返回行 alert_type 全 aml(表域收敛验证) |
6.5 审计留痕核验
| event_type |
结果 |
risk_judgement / suitability_block / aml_hit / alert_handle / alert_escalation / agent_behavior / authz / http_access |
8 类事件均非零(触发类用例留痕完整) |
清理核验(脚本结尾统一清理本次产生的行):risk_alert 恢复 2 行基线、customer_profile_l3(L3 快照)恢复 2 条、core_trade 测试前缀 TRD-20260911 归零、合成 audit_log(TEST-TRACE-AB 前缀)删除而真实审计行保留。
7. 风险发现(F1–F4,后端)
F1 · 仓储层单层防线(HIGH)
| 字段 |
内容 |
| 严重等级 |
HIGH |
| 现象 |
RiskRepository.list_alerts(customer_id=...) 与 ThresholdRepository.upsert_portfolio(customer_id=...) 自身不注入角色/归属过滤,直调即可读任意客户台账、无鉴权直写阈值配置 |
| 根因 |
鉴权仅在 API 层(app/api/risk.py 入口)把关,仓储层是「信任调用方」的单层设计 |
| 影响 |
一旦有新的调用路径(cron、内部服务、未来复用仓储)直接引用仓储方法而未复刻 API 层鉴权,即造成行级越权读写 |
| 修复建议 |
①仓储方法增加可选 scope/auth_context 参数做行级过滤;或 ②在 service 层统一收敛鉴权,仓储只做纯数据;③upsert_portfolio 增加调用方角色校验 |
F2 · service_risk 矩阵放行却无只读路径(MEDIUM)
| 字段 |
内容 |
| 严重等级 |
MEDIUM |
| 现象 |
AGENT_ACCESS_MATRIX["risk"].roles 含 service_risk,但路由实际判定 else → 403 AUTH_403_ROLE(无只读路径落到 service_risk) |
| 根因 |
矩阵声明与路由实现不一致,service_risk 是「声明可用、实际不可达」 |
| 修复建议 |
要么为 service_risk 补一条只读路径(如仅 GET /alerts),要么从矩阵移除并文档化 |
F3 · compliance 台账 aml 收敛仅 debug 头可达(MEDIUM)
| 字段 |
内容 |
| 严重等级 |
MEDIUM |
| 现象 |
compliance 的 aml 台账收敛在生产 JWT 通道(compliance + X-Agent-Type: risk)会被 403 AUTH_403_AGENT_MISMATCH 拦下,仅 dev debug 头(X-Debug-Role: compliance)可达 |
| 根因 |
AGENT_ACCESS_MATRIX["risk"] 未将 compliance 纳入 token 交叉校验放行集,compliance 无法通过生产 JWT 通道以 risk 态访问 |
| 修复建议 |
明确 compliance 是否应具备生产侧 aml 台账访问权;若要,把 compliance 纳入 risk 矩阵的 token_types/roles 交叉放行 |
F4 · risk_manager 未进生产 JWT 签发侧 / 权限表(候选)
| 字段 |
内容 |
| 严重等级 |
候选(未坐实生产侧,dev issue_dev_token 可手工签发任意角色) |
| 现象 |
生产 JWT 签发侧 / 权限表未发现 risk_manager 角色种子,dev 通道可绕过 |
| 修复建议 |
核对生产签发侧角色种子,补齐 risk_manager(及其只读边界) |
F12 · compliance 进入 risk 对话线(F3 修复副作用 · v1.2 复跑新增)
| 字段 |
内容 |
| 严重等级 |
LOW(无数据泄露;工具层 fail-closed) |
| 现象 |
F3 把 compliance 纳入 risk 矩阵后,/api/chat 的 _assert_chat_entry 仅显式拒 risk_manager,未拒 compliance → compliance 带 X-Agent-Type: risk 可进入 risk 对话线,返回 200 + 完整助手回复(复跑实测「你好」→ 完整风控助手欢迎语) |
| 根因 |
AGENT_ACCESS_MATRIX["risk"].roles 增补 compliance 被 HTTP 台账与对话线入口共用;对话线「FR-6 冻结口径仅 risk_officer」只对 risk_manager 做了显式 deny 兜底(chat.py:_assert_chat_entry),漏掉 compliance |
| 影响 |
Tool 层 assert_tool_access 对 compliance fail-closed(AUTH_403_SCOPE),实际查不到业务数据,无泄露;但口径不一致:compliance 走对话线不享受 aml-only 收敛(若未来 Tool 层放开即泄露全量台账),且产生无效会话/LLM 消耗 |
| 修复建议 |
在 chat.py:_assert_chat_entry 将 compliance 与 risk_manager 同口径显式拒 v1.3 已修(chat.py + test_chat) |
8. UX / 前端契约问题(F5–F11)
F5 · 「已处置」筛选恒 0 行(前端,严重)
RiskAlertsPage 筛选项 {value:'handled'} → ?status=handled → 后端 list_alerts 只透传 status 精确匹配,无 handled 特殊值 → 恒返回 0 行。前端把非 pending 归类为「已处置」的口径与后端无此枚举不一致。
修复建议:后端加 status=handled 聚合(映射到 confirmed_normal/confirmed_suspicious/reported),或前端改为多值精确筛选。
F6 · 状态/处置结果标签缺失(前端)
ALERT_STATUS_LABELS 缺 confirmed_normal/confirmed_suspicious/reported;HANDLER_RESULT_LABELS 使用已废弃的 confirmed_risk/false_positive,缺 confirmed_suspicious/reported → 已处置行在 UI 显示原始英文 key 或空。
修复建议:同步标签字典与后端 handler_result Literal 枚举。
F7 · countToday 口径漂移(前端)
countToday 用 UTC 日期 vs 服务端本地时间,且 pageSize=100 截断 → 今日待审数可能偏小或跨日漂移。
修复建议:由服务端提供 pending_count 统计字段,或对齐时区 + 去截断。
F8 · SSE 无心跳帧(确认)
风险对话线 SSE 长连接无心跳帧,断线感知弱、易被代理超时中断。
修复建议:加周期 heartbeat 注释帧(: 开头的 SSE 注释行)。
F9 · 三页原始 JSON 直出(前端)
simulate/suitability/aml 三页用 <pre>{JSON.stringify(result)}</pre> 直出,无结构化渲染、无错误友好提示、免责声明不常驻。
修复建议:结构化表格 + 错误码→用户可读文案映射 + 常驻免责声明。
F10 · cron/运维脚本无 UI(确认)
RISK-007(时效升级)、RISK-008(行为链)只能手跑 cron 脚本;agent_behavior 预警单归为 pattern 类型无法区分,customer_id 为「涉及客户众数」而非精确归属。
修复建议:①为升级/行为链预警提供管理端可见视图;②预警增加细分 subtype;③涉及客户精确列出(而非众数)。
F11 · 处置 409 后无自动刷新(前端,轻微)
处置 Modal 提交命中 409(他人已处置)后无自动刷新/提示,用户停留在过期状态。
修复建议:409 时自动重新拉取列表并 toast 提示。
9. 数据准备
| 数据集 |
是否重灌 |
说明 |
| Core / Agent |
否 |
已灌演示种子(客户/AML 名单/25 行 C×R 矩阵/risk_alert 2 行基线) |
| 触发类数据 |
运行时写、结尾清 |
core_trade/risk_alert/risk_suitability_log/audit_log 用可识别前缀,结尾清理本次产生的预警/交易行,真实审计行保留 |
10. 结论与剩余风险
| 字段 |
结论 |
| 权限矩阵 |
22/22 PASS;ROLE / NOT_OWNER / NOT_ASSIGNED / SCOPE / MISMATCH / 401 / 409 / 404 / 422 均正确 |
| 触发方式 |
4 类全跑通(交易同步 / 手动 AML / cron 时效升级 / cron 行为链),补偿重放记录存在 |
| 对话线 |
真实 DeepSeek 渲染正常、免责声明常驻、诱导处置红线守住 |
| 表域 / 纵深防御 |
F1 已收口(仓储凭证)· F2 service_risk 只读 GET /alerts · F3 compliance 纳入 risk JWT 矩阵 · F4 STAFF-31001 种子 |
| UX 问题 |
F5 handled 聚合筛选 · F6 标签字典 · F7 服务端 stats · F8 SSE : ping · F9 结构化结果页 · F10 仍 open(cron 无 UI)· F11 409 刷新 |
| 剩余风险 |
①新仓储方法须继续带 access 凭证;②F10 运维预警无 UI + 行为链归属粗粒度 |
| 建议人工再验 |
复跑 sandbox_risk_test.py;前端台账「已处置」筛选 + 409 回归 |
| 是否可发布 |
后端+前端契约项 v1.3 已合入工作区 · 827 pytest · 复跑 50 PASS · merger 待 commit |
11. 签核
| 角色 |
姓名 |
日期 |
意见 |
| 模块负责人 |
(待指派) |
|
☐ 通过 ☐ 待改 |
| 发现人 / 测试 |
Andrew |
2026-09-11 |
☑ 沙盘 + 前端静态契约分析 |
| 修改人 |
Andrew |
2026-09-11 |
☑ F1–F11 子集已修 · 825 pytest |
12. 修订历史
| 版本 |
日期 |
作者 |
说明 |
| v1.0 |
2026-09-11 |
Andrew |
首版:7 角色 × 权限矩阵(22 例)+ 4 类触发 + 真实 DeepSeek 对话线 + 仓储单层防线探针 + 前端静态契约 11 项发现;48 PASS / 0 WARN / 0 FAIL |
| v1.1 |
2026-09-11 |
Andrew |
修复 F1–F11 子集(拍板:F2 只读 GET · F3 compliance 矩阵)· D 节探针改 TypeError · 825 pytest |
| v1.2 |
2026-09-11 |
Andrew |
复跑 sandbox_risk_test.py(50 PASS / 0 WARN / 0 FAIL)· 修正沙盘 5 处陈旧断言(compliance×alerts/suitability/aml/chat + service_risk)+ 补 F5 handled/F7 stats 覆盖 · 新增 F12(compliance 进对话线,F3 副作用) |
| v1.3 |
2026-09-11 |
Andrew |
F12 已修:chat.py:_assert_chat_entry 拒 compliance · 单测 + 沙盘 A5 期望 403 · 827 pytest |