From 516b1132a3f4d67fb9a8b15893fe95a2e082a2cd Mon Sep 17 00:00:00 2001 From: YUAN Date: Sun, 6 Sep 2026 23:52:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20B8=20=E5=A4=8D=E5=AE=A1=20P1-1=E2=80=94?= =?UTF-8?q?=E2=80=94conftest=20except=20pytest.SkipRequested=20=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E4=B8=8D=E5=AD=98=E5=9C=A8=E5=B1=9E=E6=80=A7=E8=87=B4?= =?UTF-8?q?=20skip=20=E8=B7=AF=E5=BE=84=E7=82=B8=E6=94=B6=E9=9B=86(?= =?UTF-8?q?=E5=88=A0,=20Skipped=20=E7=BB=A7=E6=89=BF=20BaseException=20?= =?UTF-8?q?=E7=9B=B4=E4=BC=A0,=20=E9=94=99=E5=AF=86=E7=A0=81=E6=95=85?= =?UTF-8?q?=E9=9A=9C=E6=B3=A8=E5=85=A5=E9=AA=8C=E8=AF=81=20skip=20?= =?UTF-8?q?=E4=B8=8D=20error);=20P3=20=E9=A1=BA=E6=89=8B:=20teardown=20l3?= =?UTF-8?q?=5Fsnapshot=20=E5=93=A8=E5=85=B5=20+=20A-1=20=E8=A1=A5=20trace?= =?UTF-8?q?=20=E5=93=8D=E5=BA=94=E5=A4=B4/suitability=5Flog=20=E4=B8=A4?= =?UTF-8?q?=E7=8E=AF;=20P2-1=20=E6=BC=94=E7=A4=BA=E5=BA=93=E8=80=A6?= =?UTF-8?q?=E5=90=88=E5=89=8D=E7=BD=AE=E6=96=AD=E8=A8=80=E6=8C=82=20B9b=20?= =?UTF-8?q?=E6=A0=B8=E6=9F=A5=E5=8D=95=E2=91=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/memory/TODO.md | 2 +- docs/项目框架设计/开发计划-风控模块.md | 4 +++- tests/conftest.py | 4 ++-- tests/test_integration_risk.py | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/memory/TODO.md b/docs/memory/TODO.md index ed0fc81..d19f1b2 100644 --- a/docs/memory/TODO.md +++ b/docs/memory/TODO.md @@ -20,7 +20,7 @@ ### 风控模块(PRD v1.0 已冻结 · `docs/PRD/PRD-风控监测Agent.md`,事件驱动线不依赖 T-07 可先行) -- [ ] T-30 风控事件线:`app/gateway/` 交易网关 + `service/risk/` 规则引擎(RISK-001~005)+ 预警单聚合 + L3 最小写入 + `risk:pub:alert` 推送 + AML(含 `risk_aml_list` 种子)+ 演示数据脚本验收 A-1~A-5/A-9 —— **进度(2026-09-06):B1 规则纯函数 / B2 预警服务 / B3 L3 写入(risk_score 一期不写)/ B4 AML+引擎编排 / B5 交易网关 / B6 鉴权+4 API+处置编排 / B7 main 集成(路由挂载 + trace 中间件 + lifespan 启动期 debug 校验;挂账①锁公共化 ②L3 缓存 DEL ③死代码 ④统一错误体 ⑤启动校验 ⑥引擎工厂 ⑦处置原子事务均落地;⑧ input_guard_log 双写仍随 T-02)均完成并经独立 AI 评审闭环(B7 复审有条件通过→P1-1 dispose 已修闭环;183 测试绿;P2/P3 已登记开发计划 B7/B8/B9b/T-02);**B8 已完成(2026-09-06):conftest(演示数据校验/幂等代跑 prepare_risk_demo/TRD-TEST- teardown + L3 快照还原)+ 集成测试 11 例(A-1~A-5/A-7/A-9 + trace 一致性 + 审计 JSON)+ sqlite DDL 单一事实源 `_ddl.py`(10 文件收敛,localtime 时区收敛 B5 P3-4)+ L3 DEL 行为断言(P2-1)+ locks 文案(P3-2),196 绿;B8 复审待做**;剩 B9a 脚本、B9b 演示走查** +- [ ] T-30 风控事件线:`app/gateway/` 交易网关 + `service/risk/` 规则引擎(RISK-001~005)+ 预警单聚合 + L3 最小写入 + `risk:pub:alert` 推送 + AML(含 `risk_aml_list` 种子)+ 演示数据脚本验收 A-1~A-5/A-9 —— **进度(2026-09-06):B1 规则纯函数 / B2 预警服务 / B3 L3 写入(risk_score 一期不写)/ B4 AML+引擎编排 / B5 交易网关 / B6 鉴权+4 API+处置编排 / B7 main 集成(路由挂载 + trace 中间件 + lifespan 启动期 debug 校验;挂账①锁公共化 ②L3 缓存 DEL ③死代码 ④统一错误体 ⑤启动校验 ⑥引擎工厂 ⑦处置原子事务均落地;⑧ input_guard_log 双写仍随 T-02)均完成并经独立 AI 评审闭环(B7 复审有条件通过→P1-1 dispose 已修闭环;183 测试绿;P2/P3 已登记开发计划 B7/B8/B9b/T-02);**B8 已完成(2026-09-06):conftest(演示数据校验/幂等代跑 prepare_risk_demo/TRD-TEST- teardown + L3 快照还原)+ 集成测试 11 例(A-1~A-5/A-7/A-9 + trace 一致性 + 审计 JSON)+ sqlite DDL 单一事实源 `_ddl.py`(10 文件收敛,localtime 时区收敛 B5 P3-4)+ L3 DEL 行为断言(P2-1)+ locks 文案(P3-2),196 绿;B8 复审有条件通过→P1-1 conftest skip 路径炸收集已修闭环(故障注入验证),P2-1 演示库耦合前置断言挂 B9b 核查单⑥**;剩 B9a 脚本、B9b 演示走查** - [ ] T-31 `service/suitability.py` 公共校验(SUIT-001~008)+ `POST /api/risk/suitability/check` + 单测验收 A-8 —— **代码已完成(A1~A4 · 77 测试绿;B6 起该 API 带鉴权+直调审计);MySQL 手工 SQL 对照挂账至 B9b 执行(阶段 A 评审 P2-8)** - [ ] T-32 预警台账与人工处置 API(`GET /alerts`、`POST /handle`,risk_officer/compliance 权限)+ 对话线(依赖 T-01/T-03/T-07)验收 A-6/A-7 —— **API 部分已由 B6 覆盖并复审通过(A-7/A-9 用例绿);对话线仍依赖 T-01/T-03/T-07** diff --git a/docs/项目框架设计/开发计划-风控模块.md b/docs/项目框架设计/开发计划-风控模块.md index e24f453..f997c81 100644 --- a/docs/项目框架设计/开发计划-风控模块.md +++ b/docs/项目框架设计/开发计划-风控模块.md @@ -31,7 +31,9 @@ **B7 复审(独立 AI 评审 · 2026-09-06):有条件通过 → 已闭环。** 挂账①②③④⑤⑦证实落地、⑧确认随 T-02 无烂尾、红线全守住。P1-1 `dispose_engines` 仅 cache_clear 未真 dispose——已修(`utils/db.py` 手写单例字典 + 显式 `Engine.dispose()`,新增 `tests/test_db.py` 3 例,183 绿)。挂账:P2-1 L3 DEL 钩子无行为断言随 **B8**;P2-2 未捕获异常 500 无 trace 头回写 + 422/404/405 错误码补齐归 **T-02**;P3-1 `update_alert_status` 建议标注 deprecated(防绕过同事务审计)、P3-2 locks 降级文案中性化(可随 B8 顺手);P3-3 生产误配 `app_env=development` 检查归 **B9b SOP**;P3-4 独立 request_id(现沿用 trace_id)归 **T-02**。偏差留痕:成功响应不包裹外壳 = B5 P2-2「返回体不变」演进口径,M4 复核时对齐开发计划文本。 | B8 | **`tests/conftest.py`**:a) session fixture 启动校验演示数据就位(CUST-4001 测评 <365 天、risk_aml_list ≥8),缺失则中止并提示先跑 FLOW §0 ③④;b) fixture 幂等代跑 `prepare_risk_demo.sql`;c) teardown 按 `TRD-TEST-` 清 core_trade + 关联 risk_alert/risk_suitability_log/audit_log + 还原 L3 行。**顺手集中 sqlite 测试 DDL 为单一事实源(B4 评审 P3-12,各测试文件手写 DDL 收敛;CURRENT_TIMESTAMP 改 localtime 或 fixture 固定时间,防 UTC/本地日界错位——B5 评审 P3-4)**;集成测试交易统一走 `trade_id_factory` 注入 `TRD-TEST-` 前缀(trade_gateway 已留参数,B5 评审 P3-2)。集成测试:A-1~A-5、A-7(状态机/compliance 403/GET 强制 aml)、A-9 越权、**trace 一致性断言**;补 platform 审计 input_summary 的 JSON 解析断言(含引擎输出/阻断 reasons,B5 复审 L1);**B7 复审 P2-1:补 L3 DEL 钩子行为断言(fake 记录 deletes,断言 key=`profile:l3:{customer_id}` 与降级路径);P3-2 locks 降级文案中性化顺手改** | 测试套件 + fixture | `pytest` 全绿 | B7 | | B9a | 演示/运维脚本开发:`scripts/demo/subscribe_alerts.py`(订阅演示)+ `scripts/demo/rebuild_alerts.py`(按 trade_id 幂等重放补偿) | 2 个脚本 | 手工执行验证 | B2、B4(可与 B5~B8 并行) | -| B9b | 演示链路走查:`reset.ps1` → `prepare_risk_demo.sql` → agent 库建表 → `seed-aml-list.sql` → Swagger 逐条过 **A-1~A-5、A-7~A-9(A-6 归 M3)**。**B6 挂账核查单:① 预警类 API 响应体含固定 disclaimer「本预警由系统自动生成,最终判定需经风控专员人工审核」(PRD §6/规则表 §5,B6 复审 P3-7)② aml/scan 幂等防护(重复扫描同命中客户重复出单,演示点击即复现,B6 评审 P3-6)③ B6 时代码以 TestClient 独立挂 router 等价验证,本次补一次真 Swagger 手测(B6 复审遗漏⑤)④ analyst 台账只读权限扩展待 Wave 3 分析 Agent 接入时定权限矩阵(手册 §5.3 有 risk:alert:read,现 fail-closed 拒绝,B6 复审观察③)⑤ 生产/演示机 `app_env=development` 误配检查(B7 复审 P3-3:误配时 debug 头可达且启动校验放行,列入演示 SOP)** | 演示 SOP | 按 PRD §8 验收表逐条打勾 | B8、B9a | + +**B8 复审(独立 AI 评审 · 2026-09-06):有条件通过 → 已闭环。** 开发计划 B8 行 a/b/c + DDL 单一事实源/localtime + TRD-TEST- 注入 + A-1~A-5/A-7/A-9/trace/审计 JSON 全部达成,196 绿实跑,B7 复审 P2-1/P3-2 落地。**P1-1 `conftest.py` `except pytest.SkipRequested` 引用不存在属性,演示数据缺失时 skip 路径反噬整个 pytest 收集(未灌库机器单测全跑不了)——已修(Skipped 继承 BaseException 直传,故障注入验证:错密码收集期 skip 不 error)**;P3 顺手项已落:teardown `l3_snapshot` 哨兵(防 setup 失败掩盖原始异常)、A-1 补 trace 环①响应头与环②suitability_log.trace_id。挂账:**P2-1 集成测试对演示库当日状态隐式耦合(A-4 依赖 CUST-9527 当日无历史交易、A-5 aml 单查询未按本测试 trade_id 定位、A-1 同日已有 pending 单会并入旧单)→ B9b 核查单⑥,演示与 pytest 同日交叉前必须处理**;P3-3 `_state` 模块级顺序耦合(单跑 A-7 等 KeyError,文件头已声明保序,可接受留痕);P3-4 阈值冒烟用例在 .env 覆盖阈值时会红(注释已声明前提);P3-5 teardown 时间窗隐含 MySQL 时钟==本机时钟(docstring 已声明单机约定)。 +| B9b | 演示链路走查:`reset.ps1` → `prepare_risk_demo.sql` → agent 库建表 → `seed-aml-list.sql` → Swagger 逐条过 **A-1~A-5、A-7~A-9(A-6 归 M3)**。**B6 挂账核查单:① 预警类 API 响应体含固定 disclaimer「本预警由系统自动生成,最终判定需经风控专员人工审核」(PRD §6/规则表 §5,B6 复审 P3-7)② aml/scan 幂等防护(重复扫描同命中客户重复出单,演示点击即复现,B6 评审 P3-6)③ B6 时代码以 TestClient 独立挂 router 等价验证,本次补一次真 Swagger 手测(B6 复审遗漏⑤)④ analyst 台账只读权限扩展待 Wave 3 分析 Agent 接入时定权限矩阵(手册 §5.3 有 risk:alert:read,现 fail-closed 拒绝,B6 复审观察③)⑤ 生产/演示机 `app_env=development` 误配检查(B7 复审 P3-3:误配时 debug 头可达且启动校验放行,列入演示 SOP)⑥ B8 复审 P2-1:集成测试对演示库当日状态的隐式耦合前置断言(A-4 前断言 CUST-9527 当日事件计数==0、A-5 aml 单按本测试 trade_id 定位、A-1 同日 pending 单并入防护),演示与 pytest 同日交叉执行前必须处理** | 演示 SOP | 按 PRD §8 验收表逐条打勾 | B8、B9a | ## 阶段 C · 对话线(依赖 Wave 0 的 T-01 JWT / T-03 输入防护 / T-07 LangGraph) diff --git a/tests/conftest.py b/tests/conftest.py index 6f0733a..1c8b029 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,9 +62,8 @@ def ensure_risk_demo_ready() -> None: ).scalar_one() if aml_count < 8: pytest.skip(f"演示数据未就位(AML 名单仅 {aml_count} 条):{hint}", allow_module_level=True) - except pytest.SkipRequested: - raise except Exception as exc: + # pytest.skip 抛出的 Skipped 继承 BaseException,不会被此处吞掉,直接向上传播 pytest.skip(f"本机 MySQL 不可用({exc}):{hint}", allow_module_level=True) @@ -97,6 +96,7 @@ def risk_demo_env(): core = get_engine(settings.mysql_core_database) agent = get_engine(settings.mysql_database) started_at = datetime.now() + l3_snapshot: list = [] # 哨兵:setup 失败时 teardown 不因未绑定变量掩盖原始异常 try: with agent.begin() as conn: l3_snapshot = conn.execute( diff --git a/tests/test_integration_risk.py b/tests/test_integration_risk.py index c014f81..3b5e39c 100644 --- a/tests/test_integration_risk.py +++ b/tests/test_integration_risk.py @@ -107,18 +107,20 @@ def test_a1_suit001_blocked_with_trace_consistency(iclient, risk_demo_env): body = r.json() assert body["blocked"] is True assert "SUIT-001" in body["block_reason"] # C1 仅可购 R1 + assert r.headers["X-Trace-Id"] == trace # 中间件透传(PRD §7.5 全链路环①) trade_id = body["trade_id"] assert trade_id.startswith("TRD-TEST-") # 阻断不落交易(FR-1) assert _core_one(env, "SELECT * FROM core_trade WHERE trade_id = :t", t=trade_id) is None - # 校验日志 blocked + # 校验日志 blocked,trace 一致(环②) slog = _one( env, "SELECT * FROM risk_suitability_log WHERE request_ref = :t", t=trade_id, ) assert slog is not None and slog["is_blocked"] == 1 and slog["is_matched"] == 0 + assert slog["trace_id"] == trace # suitability 预警单生成(R-02 阻断单),trace 与请求一致 alert = _one(env, "SELECT * FROM risk_alert WHERE trade_id = :t", t=trade_id) assert alert["alert_type"] == "suitability" and alert["status"] == "pending_review"