Dev login no longer honors injected roles; self-service simulate/convert blocks R4 disclosure grades like the UI and chat already do. Reuse of a convert idempotency key with a different body returns 409. CT6 redeem qty is derived from T+2 lots instead of a fixed 35000; CT7-05 and CT10-07 assertions follow. Update project memory and v1.1 post-fix test artifacts.
53 KiB
企业级测试日志 · TEST-2026-09-13-CT-001
客户线交易(申购 · 赎回 · 转换 T+1 · 对话挂单 · 交易中心)—— 真实浏览器 + 真实 HTTP + 真实 MySQL 端到端验收(第四轮)
v1.1 复跑(同日 19:26 · 已取证):P0/P1/L1 已在工作区收口,并用同一套用例复跑取证 —— Part A 135 PASS / 0 FAIL / 1 SKIP / 25 INFO(退出码 0)· Part B 41 PASS / 0 FAIL / 0 SKIP / 7 INFO(退出码 0);v1.0 最大的覆盖缺口 CT6 赎回写路径已关闭(CT6 现 7 PASS)。下文 §5 已刷新为 v1.1 口径;§6/§7 的 v1.0 正文按审计惯例原样保留,并在每条缺陷上追加「复跑验证」行(企业日志不得事后改写历史结论)。原始证据:
_raw/ct-api-report-v1.1-postfix.md·_raw/results-12-customer-trade-v1.1-postfix.json。
1. 文档元数据
| 字段 | 值 |
|---|---|
| 测试记录编号 | TEST-2026-09-13-CT-001 |
| 缺陷/变更标题 | 客户线交易端到端:v1.0 收敛 2 缺陷(P0 鉴权提权 · P1 适当性单通道漏判)→ v1.1 复跑三条全绿(P0/P1/L1 已修复) |
| 文档版本 | v1.1(v1.0 初跑快照保留于 §6/§7 正文,未改写) |
| 创建日期 | 2026-09-13 |
| 最后更新 | 2026-09-13(v1.1 复跑刷新 §5) |
| 关联分支 | merger |
| 被测版本 | merger @ 2296a4a + 工作区 21 项未提交改动(git status 非干净 —— 本轮被测的就是这个未提交状态,见 §3) |
| 关联拍板 / TODO | docs/memory/TODO.md:26(本机验收项,本轮覆盖)· docs/memory/TODO.md:197(P0「E2E 最小集进仓库」,本轮部分关闭)· docs/memory/TODO.md:236-239(此前 E2E 口径教训,本轮针对性补强) |
| 风险等级 | HIGH(v1.0)→ MEDIUM(v1.1 复跑后) —— v1.0 的鉴权提权(P0)与合规单通道漏判(P1)均已在 v1.1 复跑中转绿;余 T1(仓库外脚本腐化)与 B1(既有快照不一致)两条未修 |
| 缺陷类型 | 鉴权提权 ×1 · 适当性通道级漏判 ×1 · 幂等语义潜在风险 ×1 · 测试资产腐化 ×1 · 既有基线不一致 ×1 |
| 发现阶段 | 真实浏览器 + 真实 HTTP + 真实 MySQL 端到端验收(第四轮) |
| 修复阶段 | v1.0:未修复(本轮定性「先报告、确认后再修」,app/** 与 web/src/** 零改动)v1.1:已修复并复跑验证 —— P0 落 app/gateway/jwt_service.py · P1 落 app/service/suitability.py + app/gateway/trade_gateway.py · L1 落 convert 幂等路径。复跑证据见 §5.2 |
2. 组织与责任
| 字段 | 值 |
|---|---|
| 所属系统 | JinRong 金融四 Agent 智能管家 |
| 所属模块 | 客户自助交易线(申购 / 赎回 / 转换 T+1)· 客户 Agent 对话挂单 · 客户前端交易中心 |
| 子模块 / 服务 | /api/simulate/trade · /api/simulate/trade/convert/{gid}(+/cancel) · /api/admin/convert/{confirm,meta} · /api/chat/stream · /api/customers/* · web/src/pages/customer/* · web/src/components/trade/* |
| 模块负责人 | (见 docs/项目框架设计/) |
| 发现人 | Andrew |
| 修改人 | (未修改 · 待确认后再修) |
| 测试执行人 | Andrew |
| 评审人 | (待模块负责人确认) |
| 发布建议 | 建议 P0 排修后再做对外演示:P0 在 development 下可自造任意角色 token;生产 RS256 会拒绝该 HS256 token,故当前是 dev 模式提权,不是生产直穿,但演示环境即为 dev,需先拍板。P1 是合规口径问题(需揭示产品被静默放行),不阻断功能但影响业务正确性 |
3. 环境与基线
| 字段 | 值 |
|---|---|
| 测试环境 | development · 本机 Windows 11 Pro 26200 · Python 3.13 · Node(Playwright 1.63.0,从仓库外解析,未新增仓库依赖) |
| 被测版本 | merger @ 2296a4a · git status 非干净:已修改 15 + 已暂存 6 + 未跟踪 6。本轮验收对象即此未提交工作区 —— 这是关键口径:提交后再验需重跑 |
| 后端 | uvicorn app.main:app --reload(PID 7772)· /openapi.json 66 条路径(基线 66) |
| 前端 | Vite http://localhost:5173 · HashRouter(真 URL 形如 /#/app/customer/trade?tab=subscribe&product=PROD-110022)· /api 代理 → 8000。app 无 CORS 中间件,浏览器不能直连 8000 |
| 浏览器 | 系统 Chrome(channel:'chrome' · headless)· BASE 实测 http://localhost:5173 |
| 数据库 | MySQL 3306 · jinrong_core + jinrong_agent · core_product_nav 最新 nav_date = 2026-09-11(测试日 09-13 为周日,按未知价法取 ≤ 当日最近值) |
| Redis | Docker 6380 · jinrong-redis Up (healthy) · /api/ready → ok=True degraded=False redis=True。本轮主口径为全量跑(真实抽槽路径),非降级对照 |
| DeepSeek | 有效 key · 对话线走真实 LLM(未 mock) |
| 前置自检(关键) | 脚本 preflight 校验 /openapi.json 含 6 条必需路由(/api/simulate/trade · .../convert/{gid} · .../cancel · /api/admin/convert/confirm · /api/admin/convert/meta · /api/chat/stream),缺任一即判「后端跑的是旧进程」→ 退出码 2。本轮 6/6 通过,排除最贵的假 FAIL |
| 基线对照 | 交易四文件单测 76 passed(本轮开工前实测,收工复跑仍 76 passed —— 无退步)· npm run build(tsc -b && vite build)退出码 0 |
4. 测试目标与方法
| 字段 | 值 |
|---|---|
| 目标 | 三块:A 客户交易 API(真 HTTP 直连 8000,可查库交叉印证)· B 客户前端(真浏览器经 Vite 代理)· C 既有资产回归复跑 |
| 方法 | 三条独立跑道:httpx→uvicorn / Playwright→Chrome→Vite 代理→uvicorn / pytest + tsc。断言逐用例判定,不看截图(截图仅作留档) |
| 本轮口径升级(核心) | 此前 E2E 只验「页面渲染出来 + 无 4xx/5xx」,页面上的数字、图表、交互是否正确一律未验证(TODO.md:236-239 记录的四条真缺陷即由此漏掉)。本轮逐条把前端渲染值与后端 raw 响应核对到分 |
| 用例总数 | v1.0 203 = Part A 155 + Part B 48;v1.1 209 = Part A 161 + Part B 48(Part A 增行:CT6 组由 1 条组中止占位扩为 8 条实际用例 + CT7 新增对照 7-07c 等)。(Part C 为既有套件复跑,不重复计入) |
| 账号 | 客户 CUST-9527(C3 平衡型 · 风评 2026-06-14 测 / 2027-06-14 到期)· 非本人客户 CUST-1002 · 归属顾问 STAFF-10086 · 非归属顾问 STAFF-10087 · 风控专员 STAFF-30001(risk_demo+risk_officer 同 token)· 风控经理 STAFF-31001 |
| 关键工具改进 | ① harness.mjs 与 12-customer-trade.mjs 首次入库(TODO.md:197 P0 部分关闭)② cjk() 处理 antd 6 在相邻 CJK 字间插空格的渲染特性 ③ installEventProbe()/eventCount() 以 jr:trade-completed 计数区分「真提交成功」与「被适当性拦下」 ④ until() 轮询取代固定 sleep ⑤ textOf() 收口 Playwright innerText() 的 30s 默认超时(详见 §8) |
| 未覆盖(明示) | _maintain_lots 异常分支(两轮四条写链全绿,最坏情况未出现)· SSE 顾问线对话页交互 · agent_tools.py/agent_graph.py(无 HTTP 入口) |
5. 结果总览
5.1 当前口径(v1.1 复跑 · 本节为定稿结论)
| 跑道 | 用例 | PASS | FAIL | SKIP | INFO | 退出码 |
|---|---|---|---|---|---|---|
A 客户交易 API customer_trade_e2e_smoke.py |
161 | 135 | 0 | 1 | 25 | 0 |
B 客户前端 12-customer-trade.mjs |
48 | 41 | 0 | 0 | 7 | 0 |
| 合计 | 209 | 176 | 0 | 1 | 32 |
退出码由 1 转 0 —— v1.0 的 6 条 FAIL 全部是刻意写成"应被拦下"的缺陷取证断言(P0 两条、P1 四条),修复后自然转绿,故不是"改了期望值",是同期望值的回归验证。这一点对
4-04b是例外,见 §5.3。
5.2 修复验证对照(v1.0 实测 → v1.1 实测)
| 编号 | 关键用例 | v1.0 实测 | v1.1 实测 | 判定 |
|---|---|---|---|---|
| P0 | 2-08B 顾问注入 risk_officer |
实验组 HTTP 200(越权 token 生效) | HTTP 403 AUTH_403_AGENT_MISMATCH |
✅ CLOSED |
| P0 | 2-08C 白名单外 actor 自造角色 |
对照组登录被拒 → 实验组 HTTP 200,JWT 实签发 roles=['advisor','risk_demo'] |
实验组同样 401 Unauthorized(与对照组无差) | ✅ CLOSED |
| P0 | 2-10 落库增量 = 放行笔数 |
Δ=4(含 2-08C 的越权写) | Δ=3,恰等于 2-01/03/05 三笔放行 | ✅ CLOSED |
| P1 | 4-01 / 4-02 R4 申购 |
blocked=False requires_disclosure=None match=None |
blocked=True requires_disclosure=True match=allowed_with_disclosure(两只产品一致) |
✅ CLOSED |
| P1 | 7-07 转入端 R4 |
HTTP 202 受理 | blocked=True(非 202),带 gid 但落阻断 |
✅ CLOSED |
| P1 | 7-07b 阻断的受理单零新增 |
Δ受理单 = 1 | Δ受理单 = 0 | ✅ CLOSED |
| P1 | 7-07c 对照组(v1.0 已有) |
转 R3 可自助品 → 202 受理(PASS) | 同 → 202 受理(PASS) | 两轮皆 PASS —— 这个对照组的价值恰在于它两轮不变:7-07 的差异因此只能归因于 R4 档位,而非通道本身坏了 |
| P1 | 4-04b 红线:阻断不写库 |
PASS(断言方向相反,见 §5.3) | before=104 after=104 Δ=0 |
✅ CLOSED |
| L1 | 7-05 同键不同体 |
INFO 软探针:202 accepted=True idempotent=True qty=1000.00(参数被吞、静默复用旧单) |
409 IDEMPOTENCY_CONFLICT |
✅ CLOSED |
| CT6 | 赎回写路径整组 | 组中止 → 1 SKIP | 7 PASS:6-02 复算 44922.38 与库内一致 · 6-03 FIFO 跨档(LOT-…-01 扣 30000.0000 清档 + LOT-…-02 扣 150.0000)· 6-04 快照 19850.0000 · 6-06 超份额 400 · 6-07 缺 qty 422 |
✅ CLOSED |
5.3 本轮自曝的测试资产腐化(4-04b 断言方向被反转)
v1.0 的 4-04b 标题写的是 「R4 被放行且落库(与 4-01 互为印证)」,断言 R4 两次请求应使 core_trade 增加(before=131 → after=133 Δ=2)—— 即把修复前的缺陷行为固化成了期望值。这与 scripts/dev/sandbox_risk_test.py:325-326 的 T1 腐化是同一模式:断言跟着 bug 走,bug 一修就变假红。
v1.1 已把它反转为「R4 被阻断 ⇒ core_trade 零写入」(before=104 after=104 Δ=0),与 4-04(R5)合起来构成两类阻断(需揭示 / 越级)都不落库的完整证据。脚本内已留注释说明反转理由。
这是本包内唯一被改动的测试资产;
app/**、web/src/**及既有回归脚本均零改动(见 §9.3)。
5.4 分组明细(Part A)
| 组 | 主题 | v1.1 PASS | v1.1 FAIL | v1.1 SKIP | v1.1 INFO | v1.0 对照 |
|---|---|---|---|---|---|---|
| CT1 | 前置自检与契约冻结 | 8 | 0 | 0 | 3 | 同 |
| CT2 | 鉴权矩阵 · 下单闸门 | 11 | 0 | 0 | 3 | 10 PASS / 2 FAIL(2-08B、2-08C)/ 3 INFO → 复跑后 2-08B 转 PASS、2-08C 降为 INFO(实验组不再能登录,与对照组无差) |
| CT3 | 申购放行 + 写侧对账(四链) | 17 | 0 | 0 | 4 | 同 |
| CT4 | 适当性阻断(网关唯一阻断点) | 10 | 0 | 0 | 1 | 8 PASS / 2 FAIL |
| CT5 | 申购参数校验 | 8 | 0 | 0 | 1 | 同 |
| CT6 | 赎回(FIFO 跨档 · 费率 · 超份额) | 7 | 0 | 1 | 0 | 0 PASS / 1 SKIP(组中止) |
| CT7 | 转换受理(T 日只受理不扣份额) | 14 | 0 | 0 | 1 | 11 PASS / 2 FAIL / 2 INFO(7-05 由 INFO 软探针升为硬断言 PASS) |
| CT8 | 撤单与查询(owner/scope 双闸门) | 10 | 0 | 0 | 1 | 同 |
| CT9 | 转换管理端点(仅 risk_officer) |
15 | 0 | 0 | 3 | 同 |
| CT10 | 读侧对账(下单→读侧→前端三方一致) | 12 | 0 | 0 | 4 | 同(但 10-07 v1.0 为空断言,v1.1 已实义) |
| CT11 | 对话线(SSE 帧序 + 红线) | 23 | 0 | 0 | 1 | 同 |
| CT12 | 变更清单与副作用留痕 | 0 | 0 | 0 | 3 | 同 |
5.5 业务面状态(v1.1)
| 业务面 | v1.0 | v1.1 |
|---|---|---|
下单闸门(本人 / 归属顾问 / risk_demo 放行;非本人 / 非归属 403;缺 X-Agent-Type 401) |
✅ 全绿 | ✅ 全绿 |
申购放行写链(core_trade + core_share_lot + core_holding + 读侧 四链同断言) |
✅ 全绿 | ✅ 全绿 |
净值折算复算(amount ÷ nav,与库内 core_share_rule 口径一致到分) |
✅ 全绿 | ✅ 全绿 |
| 赎回收敛(FIFO 跨档 / 费率分项 / 超份额 400) | ⚠️ 未覆盖 | ✅ 7 PASS(仅「无净值 503」数据不可达 → SKIP) |
| 转换受理 202(T 日不扣份额)· 跨主体 400 · 幂等 · 撤单 owner/scope 双闸门 | ✅ 全绿 | ✅ 全绿(+ 幂等冲突 409) |
转换管理端点(risk_officer 专属 · latest_nav_date 与库一致 · 并发锁 409) |
✅ 全绿 | ✅ 全绿 |
读侧对账(holdings / trades / products 与库逐项一致 · 无敏感字段原文 · limit 边界 · truncated) |
✅ 全绿 | ✅ 全绿 |
对话线(SSE 帧序 pending_trade 先于文本 · 挂单不写库红线 · 序号续槽 · R4 不生成挂单) |
✅ 全绿 | ✅ 全绿 |
| 前端渲染值对账(总资产 Δ · 明细行文本 · 事件派发/不派发 · 错误态 · 角色守卫) | ✅ 全绿 | ✅ 全绿 |
鉴权:roles 注入提权 |
❌ FAIL ×2 → P0 | ✅ PASS ×2(已修复) |
| 适当性:R4「需揭示」在交易通道未被强制 | ❌ FAIL ×4 → P1 | ✅ PASS ×4(已修复) |
5.6 复现确认(v1.1 定稿前实跑)
customer_trade_e2e_smoke.py --report …→ 135 PASS / 0 FAIL / 1 SKIP / 25 INFO,退出码 012-customer-trade.mjs→ 41 PASS / 0 FAIL / 0 SKIP / 7 INFO,退出码 0- 交易四文件单测 → 76 passed(与基线一致,无退步)·
npm run build→ 退出码 0 - 数据还原 → 5 张写表精确回到首值
95 / 59 / 73 / 19 / 0(见 §10.2)
⚠️ 复跑前必读:
CT3-05跨轮次顺序敏感性。 其查询窗口是audit_log.created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)(墙钟),而jinrong_agent按审计语义永不还原。故距上一轮 < 15 分钟直接复跑会假 FAIL(读到上一轮的suitability_blocked留痕)。本轮实测踩到:v1.1 早期一次复跑读到3 条残留,等待窗口自然老化(约 8.5 分钟)后重跑方得0 条 / PASS。正确口径:距上一轮 ≥15 分钟,从已还原基线单跑一次。 反向印证:3-05的这类残留只能来自被阻断的请求 —— 它本身就说明 P1 修复真的在写suitability_blocked留痕。
6. 缺陷描述(2 条缺陷 + 3 项其他发现)
⚠️ 时效声明:本节正文是 v1.0 初跑时的原始快照**,其中的「现象 / 复现 / 期望-实际」全部是修复前的实测,按审计惯例原样保留、不事后改写。每条缺陷末尾的 「✅ 复跑验证」行才是修复后的状态。读本节时请连同复跑验证行一起读,勿把「根因」段当作现存问题。 v1.1 汇总:P0 ✅ CLOSED · P1 ✅ CLOSED · L1 ✅ CLOSED · T1 ⚠️ 未修 · B1 ⚠️ 未修(既有)
归属分类:P0 = 鉴权提权(实现缺陷)· P1 = 适当性通道级漏判(合规实现缺陷)· L1 = 幂等语义潜在风险 · T1 = 测试资产腐化 · B1 = 既有基线不一致(非本轮引入)
P0 · /api/auth/login 无条件信任调用方传入的 roles,可自造任意角色 token(高)
| 字段 | 内容 |
|---|---|
| 现象 | 登录请求体里自带 roles 时,服务端原样签发,不校验请求者是谁、也不校验该 actor 是否存在 |
| 复现(两条独立探针) | ① CT2-08B:以 STAFF-10087(真实存在但无风控角色)登录时注入 roles:["risk_officer"] → 拿该 token 调 /api/risk/alerts:对照组(不注入)HTTP 403 AUTH_403_AGENT_MISMATCH → 实验组 HTTP 200② CT2-08C:以 STAFF-99999(不在任何白名单内)注入 roles:["advisor","risk_demo"] → 对照组登录即失败 → 实验组 HTTP 200,且成功代 CUST-1002 下单(trade_id=TRD-20260913-BA175EB6,真实落库) |
| 期望 / 实际 | 注入角色应被拒绝(403/401)→ 状态码被注入翻转 |
| 根因 | 两处叠加: ① app/gateway/jwt_service.py:118-120 —— infer_roles() 首行即 if roles: return roles,对调用方传入的角色无任何校验(默认角色表 DEFAULT_ROLES_BY_ACTOR 被完全绕过);② app/api/auth.py:16-44 —— /api/auth/login 无 app_env 门禁,且 app/main.py:103 无条件挂载该路由。jwt_ready() 的守卫只覆盖 main.lifespan 的「能否启动」,不覆盖「启动后可调用哪些路由」 |
| 影响面 | development 下可提权到任意已注册角色(含 risk_officer 的转换确认批处理、advisor 的代客下单)。生产以 RS256 签发,该 HS256 token 会被拒 —— 故不是生产直穿,而是 dev/演示环境的提权。但演示环境即 dev,且 §3 已确认本轮被测版本正是 dev 配置 |
| 纵深防御现状 | 未被完全击穿:CT2-09 证明拒绝路径有审计留痕(近 20 分钟 30 条 AUTH_* 记录,落 app/utils/authz.py:record_authz_denial → audit_log(event_type='authz') + input_guard_log)。即提权可被事后发现,但不能被阻止 |
| 修复方向(只写方向) | ① infer_roles() 忽略外部传入的 roles,一律查 DEFAULT_ROLES_BY_ACTOR;② /api/auth/login 增设 app_env != production 门禁或直接不挂载;③ 对 actor 做存在性校验(STAFF-99999 本不该签发)。 |
| ✅ 复跑验证(v1.1 · 2026-09-13 19:26) | CLOSED。实际修复落在方向①:app/gateway/jwt_service.py:115-125 的 infer_roles() 改为 _ = roles(显式丢弃调用方传入值),一律查 DEFAULT_ROLES_BY_ACTOR,白名单外 actor 返回 []。实测三处同时翻转:2-08B 实验组 403 AUTH_403_AGENT_MISMATCH;2-08C 实验组登录即 401(与对照组无差);2-10 落库增量 Δ=4 → Δ=3,恰等于三笔放行写。⚠️ 残留(未验):方向②③均未实施 —— /api/auth/login 仍无 app_env 门禁、仍无条件挂载。本轮只证明「注入 roles 不再生效」,未证明「非白名单 actor 一律无法登录」在生产配置下的行为。若日后有人改回 infer_roles 的 if roles 语义,缺陷会原样复现 |
P1 · R4「需揭示」产品在交易网关被当普通放行,申购与转换两通道同时漏判(中)
| 字段 | 内容 |
|---|---|
| 现象 | C3 客户申购 R4 产品(适当性结论 allowed_with_disclosure,应走「揭示书 + 双录」流程),POST /api/simulate/trade 返回 200 且 blocked=false,静默放行;转换通道同形 |
| 复现(两通道四探针) | 申购:CT4-01 PROD-161726 → HTTP 200 blocked=False requires_disclosure=None match=None;CT4-02 PROD-510500 → 同形(排除单品配置问题 ⇒ 通道级判定缺失)转换: CT7-07 转入端 R4 PROD-161725 → HTTP 202(受理成功) 而非 200+blocked;CT7-07b 阻断单零新增断言 → Δ受理单 = 1(本应 0) |
| 期望 / 实际 | 响应须体现需揭示(requires_disclosure=true 或 blocked=true)→ blocked=False requirements_disclosure=None |
| 根因 | app/gateway/trade_gateway.py:534 —— 适当性字段(requires_disclosure / match_result / block_response_code / rule_refs / reasons / advice / notice)只在 blocked 分支内组装返回。allowed_with_disclosure 的 blocked=False,于是这些字段整体丢失,调用方无从知道该单需要揭示 —— 不是「判定错了」,而是判定结果没被传出来 |
| 关键佐证(三处已判 R4 不可自助,仅网关漏判) | ① 前端 web/src/utils/tradeEligibility.ts:29-31 判 R4 非自助(CT4 对应 B7 实测:R4 产品「申购」按钮 disabled 且标注「需揭示/网点办理」)② 对话线 subscribe_self_service_allowed()(app/service/trade_action_service.py:88-91)判 R4 非自助(CT11-04b3 实测:R4 条目被明确标注「本助手不可自助申购」,且选其序号不生成挂单)③ /api/customers/{id}/products 正确返回 match_result=allowed_with_disclosure + requires_disclosure=1(CT10-06c 实测)⇒ 三者一致判 R4 不可自助,只有交易网关把它当普通放行。这是单点实现缺失,不是设计分歧 |
| 影响面 | 合规口径:需揭示产品可被客户在交易中心与转换通道无揭示地买入。功能不阻断,但业务不正确 |
| 修复方向(只写方向) | 网关在 allowed_with_disclosure 时也返回适当性字段并把该分支并入 blocked(或新增中间态),使前端能弹揭示书。 |
| ✅ 复跑验证(v1.1 · 2026-09-13 19:26) | CLOSED。修复采用「收敛到单一判据」而非在两处各打补丁:新增 app/service/suitability.py:84-94 simulate_self_service_blocked(result),把 blocked / needs_branch_confirm / match_result=="allowed_with_disclosure" / requires_disclosure 四种情形统一判为「自助通道视同阻断」;两条通道同时改为调用它 —— app/gateway/trade_gateway.py:534(申购)与 app/service/convert/convert_service.py:490(转换)。实测四探针全转绿:4-01/4-02 blocked=True requires_disclosure=True match=allowed_with_disclosure;7-07 由 202 转 blocked=True;7-07b Δ受理单 1 → 0。⚠️ 语义变更(需业务确认):修复把 allowed_with_disclosure 归入阻断,即 R4 产品的自助通道由「静默放行」变为完全不可自助下单(原设计是「可买但需弹揭示书」)。这与前端 tradeEligibility.ts 的既有口径一致,但等于收窄了业务能力 —— 若业务上期望「弹揭示书后可继续购买」,则本修复是过渡方案,需另开「揭示书 + 双录」续接流程 |
L1 · 幂等键相同但请求体不同 → 静默返回原单(低 · 潜在风险)
| 字段 | 内容 |
|---|---|
| 现象 | CT7-05:同 client_request_id、不同请求体重放 → 返回首次那笔单,无任何冲突提示(既非 409 也非新单) |
| 根因 | 幂等键只做「命中即回放」,未比对请求体指纹 |
| 影响面 | 调用方以为自己提交了新单,实际拿到旧单 —— 在重试语义下可能造成金额误解。本轮未观察到实际危害(CT7-04 同键同体幂等行为正确) |
| 修复方向 | 命中幂等键时比对 body 摘要,不一致返回 409。 |
| ✅ 复跑验证(v1.1 · 2026-09-13 19:26) | CLOSED。实测 7-05 → HTTP 409 IDEMPOTENCY_CONFLICT,且用例已由 INFO 软探针升为硬断言 PASS(契约由此冻结)。7-04(同键同体)仍正确回放原单 —— 说明修复只切断了「同键不同体」这一支,未误伤正常幂等 |
T1 · 前端登录页重写令仓库外三支回归脚本集体失效(测试资产腐化 · 本轮已临时修补)
| 字段 | 内容 |
|---|---|
| 现象 | web/src/pages/login/LoginPage.tsx 由「一键 Demo 账号按钮」重写为表单(demoAccounts.ts 的 DEMO_ACCOUNTS 已置空并标 @deprecated),仓库外 C:/Users/Windows/e2e-jinrong/harness.mjs 的按钮式 login() 因此全线超时 |
| 实测影响 | 09-advisor-pages.mjs · 10-risk-pages.mjs · 11-cross-role.mjs 三支全部无法启动(Timeout … waiting for getByRole('button', { name: /客户 Demo/ })) |
| 处置 | 仅替换该 harness 的 login() 为「填账号 → 点登录」(备份 harness.mjs.bak-pre-20260913),既有断言一字未改;随后三支恢复并转绿(见 §7 节 C) |
| 残留 | 11-cross-role.mjs 的 C1.6 断言前提已失效(该断言假定「顾问页对风控用户会打开」,而 F3 路由守卫修复后风控用户被正确重定向)→ 是陈旧断言,非缺陷,已记入 §8 |
B1 · 既有基线不一致:core_holding 快照与 SUM(core_share_lot.remain_qty) 不符(非本轮引入)
| 字段 | 内容 |
|---|---|
| 现象 | CT1 基线探针:8 只产品中 3 只对不上。PROD-000001 快照 20000.0000 vs 批次和 49400.0000;PROD-005827 15000.0000 vs 1223053.7000;PROD-110022 96172.1500 vs 7350802705.5200 |
| 性质 | 本轮之前既有,非本轮写侧造成。其中巨型批次 LOT-SUB-TRD-20260913-68DEC409 恰为 1e10 ÷ 1.3604 的量级,疑为历史手工/脚本灌数留下的越界批次 |
| 本轮处置 | 不改。所有一致性断言一律改用「前后差值」而非绝对值(CT3-01 四链断言即以此成立),故此不一致未污染任何用例结论 |
| 建议 | 单独排查该巨型批次的来源;本轮不动 |
7. 测试执行记录(明细)
⚠️ 本节是 v1.0 初跑的逐组明细,其中的判定列(如「2 FAIL(P0)」「1 SKIP(覆盖缺口)」)是修复前的状态,按审计惯例原样保留。v1.1 的对应状态见 §5.4 分组明细表与 §5.2 修复验证对照。本节的价值在于保留「修复前每组长什么样」这一手证据。
Part A · 客户交易 API(v1.0 · 155 用例 · 真 HTTP + 真库交叉印证)
| 组 | 关键实测 | 判定 |
|---|---|---|
| CT1 契约冻结 | /api/ready ok=True degraded=False redis=True · openapi 66 路径 · 6/6 必需路由在位 · 14 只产品 / 10 只标的全有净值 · 字段面 alert_ids(复数)存在、alert_id(单数)不存在 |
✅(+3 INFO,含 B1 基线不一致) |
| CT2 鉴权矩阵 | 2-01 本人 → 200 · 2-02 非本人 → 403 AUTH_403_ROLE · 2-03 归属顾问 → 200 · 2-04 非归属顾问 → 403 · 2-05 risk_demo → 200 · 2-06 无 token → 401 AUTH_401_MISSING_DEBUG_HEADERS · 2-07 缺 X-Agent-Type → 401 AUTH_401_MISSING_AGENT_TYPE · 2-10 放行笔数 = 落库增量(before=122 after=126 Δ=4,拒绝类贡献 0)— 证明无请求从拒绝路径逃逸到写库 |
2 FAIL(P0) |
| CT3 申购写链 | R1 已持有品(110022 10000 元 → Δ批次 7350.7800 = 10000÷1.3604)、R2 未持有品(005828 5000 元 → 1744.5900 = 5000÷2.8660)—— 每笔皆四条链同断言:① core_trade 新行 ② core_share_lot 批次变动 = 折算份额 ③ core_holding 快照变动 ④ GET /holdings 返回值变动。R1 风险点(_maintain_lots 静默失败)本轮未复现 |
✅ 17 PASS |
| CT4 适当性阻断 | 4-03 R5 PROD-XYZ999 → 200+blocked=true forbidden ✅ · 4-04 阻断不写 core_trade(零增量)✅ —— 本轮最关键红线之一 · 4-05 未知客户 fail-closed SUIT_NOT_FOUND ✅ · 4-01/4-02 R4 两只产品行为一致 → FAIL |
2 FAIL(P1) |
| CT5 参数校验 | 缺 amount / 0 / 负数 / 缺 product_id / client_request_id 非法 → 422 · 未知 trade_type → 400(非 422,刻意保留的既有口径) · 未知产品 → 200+blocked(非 404,fail-closed) |
✅ 8 PASS |
| CT6 赎回 | 整组提前返回 —— t2_lots(CUST-9527, PROD-005827) 的 T+2 可用批次合计 15000.0000 < 用例写死的申报 35000 |
⚠️ 1 SKIP(覆盖缺口) |
| CT7 转换受理 | 7-01 110022→110023 → 202 + convert_group_id + 三日期全非空 ✅ · 7-02 T 日不扣份额(仅 core_convert_request +1)✅ · 7-03 跨主体 → 400 CROSS_ENTITY_NOT_SUPPORTED ✅ · 7-04 同键同体幂等 → 同 gid 仅 1 行 ✅ · 7-07/7-07b 转入端 R4 → 202 受理且受理单 +1 → FAIL |
2 FAIL(P1) |
| CT8 撤单 / 查询 | 8-01b 未确认态不返回折算金额 ✅ · 8-02 归属顾问可查同一张单 → 200 ✅ · 8-03 归属顾问不可撤 → 403 AUTH_403_ROLE ✅(查询 scope 与撤单 owner 是两类闸门,不可合并 已实证)· 8-05 非本人查询 → 403 AUTH_403_NOT_OWNER ✅ · 8-07 重复撤单 → 409(状态闸门先于时间闸门)✅ · 8-08 不存在的单 → 404 ✅ |
✅ 10 PASS |
| CT9 管理端点 | 9-01 risk_officer 专属 · latest_nav_date 与库 MAX(nav_date) 一致 ✅ · STAFF-31001(risk_manager)→ 403 ✅ · 9-04 confirm?accept_date=2026-09-11 放行 ✅ · 9-06 非法日期 → 400 ✅ · 9-08 外部预占锁 → 409 CONCURRENT_CONFLICT(确定性,非赛跑型 flaky)✅ |
✅ 15 PASS |
| CT10 读侧对账 | 10-01b holdings 与 core_holding 8 行全一致(不一致 0 项)✅ · 10-04 申购后 PROD-510300 从无行变有(qty=8734.76 = Σ批次 8734.7600,差值为 0)✅ · 10-03 limit=0/501 → 422 边界 ✅ · 10-02 无身份证/手机原文 ✅ · 10-06c PROD-161725 → allowed_with_disclosure、PROD-XYZ999 → forbidden(前端适当性数据源正确)✅ |
✅ 12 PASS(10-07 为空断言,见 §11) |
| CT11 对话线 | 11-02b pending_trade 帧@2 先于首个文本 delta@3(帧序断言,非「出现过」)✅ · 11-03 红线:对话挂单不写 core_trade(135 → 135)✅ · 11-04c 序号全局连续递增 1→14 不回绕 ✅ · 11-04d 选 R4 序号 不生成挂单且不写库 ✅ · 11-05 序号续槽命中(不重出清单,Redis 真实抽槽路径)✅ · 11-07 不存在的 session_id → 404 fail-closed ✅ · 11-08 顾问线流式不回归 ✅ |
✅ 23 PASS |
| CT12 副作用留痕 | 五表首尾计数差(见 §10)· 5 行受理单带前缀 E2E-CT-20260913175209 可识别 · trade_id 为生产口径 TRD-<date>-<uuid8> 不可按前缀识别 ⇒ 还原必须依赖快照而非前缀删除 |
3 INFO |
Part B · 客户前端(v1.0 · 48 用例 · 真浏览器 + 真实 LLM)
⚠️ v1.0 的 Part B 跑在 Part A 之后、还原之前,故其资产基线是脏的 ——
B1.6读到339,728.12/ 10 只(含 Part A 未还原写入)。v1.1 从已还原基线起跑,B1.6=169,298.06/ 6 只,并连带使B2.0取到真正未持有品、B3.3转 PASS、B7.0取样点分离。详见 README「v1.0 Part B 基线污染」段。
| 用例 | 关键实测 | 判定 |
|---|---|---|
| B1 交易中心三 Tab | 三 Tab 就位 · 无 ErrorBoundary · issues.httpErrors 为空 · 前基线:总资产 339,728.12 · 持仓 10 只 |
✅ |
| B2 申购提交 | 经真实 UI 路径:填 .ant-input-number-input → 「下一步:确认」→ Modal 内「确认提交」→ [data-testid="trade-submit-done"] 出现且为成功图标(anticon-check-circle + text-[var(--jr-positive)])· Modal 在 1938 ms 自动关闭(断言 ≤2500 ms,SUCCESS_DISMISS_MS=1400) · jr:trade-completed 派发 0 → 1 |
✅ |
| B3 首页数字对账 | 总资产 339,728.12 → 352,073.79,Δ = 恰好 12,345.67(提交金额,核对到分) · 同页赎回下拉即时出现刚买入的产品(handleTradeSuccess → reloadCatalog)· 手动派发事件 → 真实触发 /holdings 重新请求(事件消费方接线独立于路由被证明)· B3.3 SKIP(标的已持有) |
✅ |
| B4 交易明细 | 最新行文本 2026/09/13 18:28 申购 沪深300指数 R3 12,345.67 元 已确认 —— 前端渲染值与后端 raw 响应对齐到分 · 类型列中文标签正确(convert 亦由 labelTradeType 覆盖) |
✅ |
| B5 R5 反向断言 | R5 产品经 URL 预填绕过 disabled 选项进入表单 → 提交后 Modal 显示阻断图标 / 未通过校验 · 未派发 jr:trade-completed(2 → 2) · 首页数字一位未变(352,073.79 / 持仓仍 10 只) · 明细无新行 |
✅ |
| B6/B7 市场页与快捷交易 | 「交易」列对客户角色渲染(表头 [产品代码,产品名称,类型,风险,最新净值,日涨跌,净值日期,交易])· 申购 → ?tab=subscribe&product= ✅ · 走势/赎回 → #/app/market/{id} ✅ · 转换 → ?tab=convert&from= ✅ · 未持有品三键全 disabled(申购(需揭示/网点办理)/赎回(无持仓)/转换)· 持有品赎回按键为 赎回(持仓 107198.74 份) 且可点 · R4 品「申购」disabled 标注「需揭示/网点办理」(与 P1 相互印证:前端判 R4 非自助)· 顾问角色不渲染快捷交易(命中=0) |
✅ |
| ⚠️ B7 取样局限(如实声明) | 本轮 B7.0 实测「未持有=PROD-003095 · R4=PROD-003095」—— 两者恰为同一只产品。故「未持有 ⇒ 赎回 disabled」与「R4 ⇒ 申购 disabled」两条结论落在同一个取样点上。B7.1 的按钮文案 赎回(无持仓) 仍独立证明了赎回闸门,但**「未持有且非 R4」的干净样本本轮没取到**(可自助未持有品已被多轮 E2E 买空,同 B3.3 的成因) |
— |
| B8 对话挂单(真实 LLM) | 「我能买什么」→ 清单含 【R1】…【R3】 + 「请回复上方序号」· 回复 1 → 索取金额(不重出清单)· 回复 5000 → Modal 自动打开,显示 现金宝货币(PROD-000001) / 申购金额 5000 元 · 红线守住:仅弹 Modal,未派发事件、未写库 |
✅ |
| B9 F3 路由守卫回归 | 顾问直达 /app/customer/trade → hash 最终为 /app/advisor/home(重定向,非页面内警示) |
✅ |
| B10 接口错误态 | 经 page.route 强制 500 → ApiErrorResult 渲染 code E2E_FORCED_500 / message E2E 注入的 500 / trace_id —— 补齐 TODO.md P1「接口错误态」缺口 |
✅ |
| B11 真登录路径 | 不 reload 的表单提交直达 → 无 ErrorBoundary、三 Tab 就位 | ✅ |
| B3.3 | SKIP:标的已持有,只数不变属预期 | ⚠️ SKIP |
Part C · 回归复跑(既有资产 · 4 条跑道)
| 跑道 | 命令 | 结果 | 对照 |
|---|---|---|---|
| 交易单测 | pytest tests/test_trade_gateway.py test_trade_flow_service.py test_trade_action_service.py test_convert_confirm.py -q |
76 passed | 与开工基线一致,无退步 |
| 类型闸门 | cd web && npm run build(tsc -b && vite build) |
退出码 0 | 无 TS 回归 |
09-advisor-pages.mjs |
顾问前端六页 | 17 PASS / 0 FAIL | 上轮 15 PASS / 2 FAIL → 转绿 |
10-risk-pages.mjs |
风控前端 | 21 PASS / 0 FAIL | 上轮 19 PASS / 2 FAIL → 转绿(F1/F2 已修) |
11-cross-role.mjs |
跨角色 | 8 PASS / 1 FAIL | 上轮 4 PASS / 5 FAIL → F3 翻转(C1.2–C1.5 现为「打开=false hash=#/app/risk/home」即正确重定向);余 1 FAIL(C1.6)为陈旧断言,非缺陷 |
risk_e2e_smoke.py --only R6 |
转换确认批处理(风控视角交叉印证) | 23 PASS / 0 FAIL / 0 SKIP,退出码 0 | 其中 区间外 2026-09-15 → scanned=25 confirmed=0 nav_pending=25 —— 独立复现 FR-C27 nav_pending 语义 |
Part C 的 09/10/11 三支在跑前被 T1(登录页重写)击穿,修补 harness
login()后恢复 —— 上表结果是修补后的。这是本轮「既有资产腐化」的实证。
8. 非缺陷澄清(易误判项 · 本轮实测踩过)
| # | 易误判为缺陷的现象 | 实测结论 |
|---|---|---|
| 1 | R4 产品按理应「阻断」 | R4 的正确结论是 allowed_with_disclosure(blocked=false)—— 不阻断。P1 的要害是「需揭示」这一信息没被传出来,不是「应该 403」。本轮 FAIL 的期望值写的是「响应须体现需揭示」,措辞已避开这个歧义 |
| 2 | 未知产品返回 200 而非 404 | 计划原写 404,实测为 200 + blocked=true + SUIT_NOT_FOUND(fail-closed)。这是既有设计口径,非缺陷;脚本期望值已按实测冻结 |
| 3 | 对话清单列出了 R4/R5("不该列出"?) | 货架口径本就要列全,区别在于标注:R1–R3 标「可直接申购」,R4/R5 标「本助手不可自助申购」。CT11-04b3 实测 8 只可直接申购 + 5 只明确标注 ⇒ 正确 |
| 4 | 自造 session_id 返回 404 |
是 chat.py:132-136 _guard_session 的刻意 fail-closed(避免串号),非 bug。客户端本就应从首帧 meta.session_id 取服务端下发的 id |
| 5 | 交易明细表首屏恒为 10 行 | CustomerTradesPage 的 pagination={{pageSize: 10}} ⇒ 新交易把最旧一行挤出首屏,行数永远涨不上去。故 B4.1/B5.4 改判「最新一行内容正确」与「首行指纹变化」,而非行数增加 |
| 6 | 首页「总资产」取不到(返回 null) | 它是 Typography.Text 的 <span> 而非标题元素 ⇒ 按 h1/h2/h3 找不到。且 useCountUp 有滚动动画,需采样至两次读数一致再断言(否则误判为数字不符) |
| 7 | 申购 按钮的文本匹配失败 |
antd 6 在相邻两个 CJK 字符间插空格 ⇒ 裸二字按钮渲染为 "转 换"。申购/赎回 因带后缀(申购(…))而幸免。已在 harness 提供 cjk() 统一处理 |
| 8 | .ant-select-selection-item 取不到已选值 |
该 class 在 antd 6 已不存在(这是 antd 5 的写法)。antd 6 用 .ant-select-content。此外隐藏的已访问 Tab 面板会保留在 DOM 中 ⇒ 必须用 .ant-select:visible(实测 .ant-select 计数 2、:visible 计数 1) |
| 9 | 脚本挂了 15 分钟不动 | Playwright innerText() 默认 30 s 超时,被 .catch(() => '') 吞掉后表现为「静默慢」⇒ 20 次轮询 = 10 分钟。已用 textOf() + T={txt:1200,quick:1500} 收口 |
| 10 | B3.3 持仓只数没变(10 → 10) |
测试数据耗尽:多轮 E2E 已把可自助的未持有品全部买入持仓,脚本回退到已持有的标的。脚本据此判 SKIP 而非 FAIL 是正确处置(否则是假 FAIL) |
| 11 | 11-cross-role.mjs 的 C1.6 仍 FAIL |
该断言前提已失效(假定顾问页对风控用户会打开),而 F3 守卫修复后风控用户被正确地重定向走 ⇒ 陈旧测试资产,不是应用缺陷 |
| 12 | CT10-07 赎回后读侧一致 PASS |
空断言:CT6 未实际发生赎回,状态本就不变。不得计入赎回能力证据(见 §11) |
| 13 | CT2-06 无 token 返回 AUTH_401_MISSING_DEBUG_HEADERS 而非 ..._BEARER |
dev 下的调试头回退路径(无 Bearer 且 app_env == development 时改查 X-Debug-Role/X-Debug-Actor)。是既有设计,非缺陷 |
9. 变更清单
9.1 本轮新增(全部为测试资产 · 业务代码零改动)
| 文件 | 类型 | 说明 |
|---|---|---|
scripts/dev/customer_trade_e2e_smoke.py |
新建 | Part A 载体,CT1–CT12 / 155 用例。继承 risk_e2e_smoke.py 的 preflight 路由自检 + 退出码约定(0/1/2),新增双头构造器(trade_h 带 X-Agent-Type / platform_h 不带)· sse_frames() 逐帧捕获 · --report 落盘 |
scripts/e2e/harness.mjs |
新建 | 浏览器 E2E 共用骨架(从仓库外收敛入库)。Playwright 由 PW_HOME 解析,未往 web/package.json 加依赖 |
scripts/e2e/12-customer-trade.mjs |
新建 | Part B 载体,B1–B11 / 48 用例 |
docs/memory/tests/2026-09-13-customer-trade-e2e/** |
新建 | 本证据包(README + 本日志 + _raw/) |
docs/superpowers/plans/2026-09-13-customer-trade-e2e.md |
新建 | 本轮计划的归档副本 |
9.2 本轮修改
| 文件 | 位置 | 说明 |
|---|---|---|
harness.mjs 的 login() 函数 |
仓库外 C:/Users/Windows/e2e-jinrong/ |
仅此一个函数:按钮式 → 「填 input#loginId → 点表单登录按钮」。既有断言一字未改;备份 harness.mjs.bak-pre-20260913 |
9.3 本轮未改动(边界 · 明确声明)
app/**、web/src/**—— 业务代码零改动(缺陷只报告不修)。git status中app/与web/src/下的改动均为本轮开工前既有的未提交工作,即本轮的被测对象,非本轮所加。tests/conftest.py·scripts/core/reset.ps1·scripts/dev/restart-dev.ps1—— 未动。scripts/dev/advisor_e2e_smoke.py·scripts/dev/risk_e2e_smoke.py—— 未动(它们是回归基线,改了会让「FAIL 转 PASS」的对照失效)。- T1 相关腐化项(
scripts/dev/sandbox_risk_test.py仍断言 convert→400 ·scripts/demo/run_demo_smoke.ps1仍调已不存在的/api/v1/*)· D3(Milvus 集合released未load())—— 只记录不修。 - 未新增任何 Python 依赖(
httpx/pymysql既有)· 未新增浏览器依赖。
10. 数据准备与副作用
10.1 三层防线
- 第 1 层(贯穿全程):断言一律用「前后差值」,禁止绝对计数。
before在同一条用例内紧邻采集;批次类断言按业务键(WHERE customer_id=? AND product_id=?)定位而非行号/主键;幂等键一律E2E-CT-<yyyymmddHHMMSS>-<nn>每轮唯一(固定键重跑会命中上一轮的单,语义就变成「非首次受理」)。这是为什么 §6 B1 的既有基线不一致没有污染任何结论。 - 第 2 层(本轮采用):跑前
mysqldump快照 5 张写表,跑后还原。 - 第 3 层(本轮未使用):
scripts/core/reset.ps1—— 它会 DROP 重建jinrong_core(丢全部手工演示态)且不动jinrong_agent,导致 agent 库残留指向已消失 Core 数据的引用,使「用 agent 库对账」类用例结论不可信。本轮快照可用,故未启用、基线未变更。
10.2 快照与还原(本轮实测)
| 项 | 值 |
|---|---|
| 快照命令 | mysqldump -h127.0.0.1 -uroot jinrong_core core_trade core_holding core_share_lot core_convert_request core_convert_lot_detail --single-transaction --add-drop-table |
| 快照文件 | /tmp/ct-e2e/snapshot-core-5tables.sql(44291 bytes,含恰好 5 张表的 DROP TABLE IF EXISTS) |
| 还原前 | 先对跑后状态另做一份备份(/tmp/ct-e2e/post-run-backup.sql,63055 bytes)—— 使还原动作本身可逆,避免「还原错了无法回退」 |
| 还原命令 | mysql -h127.0.0.1 -uroot jinrong_core < /tmp/ct-e2e/snapshot-core-5tables.sql → 退出码 0,stderr 空 |
五表计数 —— 精确回到首值:
| 表 | 快照时点(首) | 全轮结束后(尾) | Δ | 还原后 | 判定 |
|---|---|---|---|---|---|
core_trade |
95 | 140 | +45 | 95 | ✅ 回到首值 |
core_holding |
59 | 66 | +7 | 59 | ✅ 回到首值 |
core_share_lot |
73 | 117 | +44 | 73 | ✅ 回到首值 |
core_convert_request |
19 | 34 | +15 | 19 | ✅ 回到首值 |
core_convert_lot_detail |
0 | 0 | 0 | 0 | ✅(转正确认批处理未跑,故恒 0) |
还原后最后一行
core_trade为TRD-20260913-2DBB1BDD(17:10 的手工演示单)—— 证明本轮之前的既有演示态被完整保留,被回滚的只有本轮 E2E 产生的行。 Part A 进程内自记的 CT12-01 差值(core_trade 121→135 Δ+14·core_holding 64→64 Δ+0·core_share_lot 98→112 Δ+14·core_convert_request 24→29 Δ+5)只覆盖 Part A 窗口,与上表的「整轮」口径不同,两者不矛盾。
v1.1 复跑轮的快照与还原(同日 19:06 → 19:27)
| 项 | 值 |
|---|---|
| 快照文件 | C:/Users/Windows/AppData/Local/Temp/ct-fix/snapshot.sql(45658 bytes,19:06 · --single-transaction --add-drop-table) |
| 起跑基线 | 95 / 59 / 73 / 19 / 0 —— 与 v1.0 快照的起始值完全一致,这本身就是 v1.0 还原成功的独立佐证 |
| 还原前(跑后) | 106 / 61 / 83 / 23 / 0 |
| 还原后 | 95 / 59 / 73 / 19 / 0 —— ✅ 精确回到首值 |
Part A 进程内自记的 CT12-01(v1.1):
core_trade 95 → 106 (Δ+11)·core_holding 59 → 61 (Δ+2)·core_share_lot 73 → 83 (Δ+10)·core_convert_request 19 → 23 (Δ+4)·core_convert_lot_detail 0 → 0 (Δ+0)。这次 CT12-01 与「整轮」口径恰好相等(因 v1.1 的 Part A 是单独一轮、前后各还原一次),两者互为校验。 可识别性:12-02报告「4 行带前缀E2E-CT-20260913192609」;12-03「近 60 分钟core_trade11 行(含本轮前手工演示)」。trade_id仍是生产口径TRD-<date>-<uuid8>,不可按前缀识别 ⇒ 还原必须依赖快照,不得依赖前缀删除 —— 这条 v1.0 的结论在 v1.1 依然成立。
10.3 刻意不还原的对象
core_product/core_product_nav/core_customer_advisor/ 交易日历 —— 本轮只读,还原会破坏基线。jinrong_agent的risk_alert/risk_suitability_log/audit_log—— 追加型审计日志,按语义不应回滚(回滚反而破坏审计链)。P0 的AUTH_*拒绝留痕即在此,保留后可被第三方查阅。
10.4 测试数据可识别性
- 受理单幂等键带前缀
E2E-CT-20260913175209(5 行),可被第三方识别与单独清理。 - 但
trade_id为生产口径TRD-<date>-<uuid8>,不可按前缀识别 ⇒ 本轮还原必须依赖快照,不能靠前缀删除。此为 CT12-03 的实测结论。
11. 结论与剩余风险
11.1 结论
- 主流程全绿。 下单闸门、申购写链(四链同断言)、赎回写链(FIFO 跨档 · 费率分项 · 超份额)、转换受理与 T 日不扣份额、撤单双闸门、管理端点、读侧三方对账、对话线帧序与两条红线(对话挂单不写库 · 阻断不写库)全部通过。
- v1.0 的 6 条 FAIL 收敛为 2 个缺陷,均给到根因(
jwt_service.py:118-120+auth.py无门禁;trade_gateway.py:534仅 blocked 分支返回适当性字段),且 P1 由两条通道 × 四探针复现、并与前端/对话/接口三处正确判定交叉印证 ⇒ 单点实现缺失,非设计分歧。 - v1.1 三条缺陷(P0/P1/L1)全部 CLOSED 且已复跑取证 —— 退出码由 1 转 0,期望值一字未改(唯一例外
4-04b的反转已单独声明,见 §5.3)。这满足「修复必须可验证」的要求:不是删掉失败用例,而是同一条断言转绿。 - v1.0 最大覆盖缺口(CT6 赎回写路径)已关闭 —— 脚本改为由库内事实推导申报量(
derive_redeem_qty),CT6 由「1 条组中止 SKIP」变为 7 PASS;连带解除了CT10-07的空断言状态。 - 本轮口径升级达成:前端渲染值首次与后端 raw 响应核对到分(总资产 Δ 恰为提交金额 · 明细行文本对齐 · 事件派发/不派发可区分)。这补上了此前三轮「只验渲染、不验数字」的缺口。
- 回归无退步,且三条既有缺陷转绿(F1/F2/F3)—— 与
TODO.md记录的「已修」一致,本轮独立复现了该结论。v1.1 复跑后 76 passed /npm run build退出码 0 均维持在基线。 - 数据副作用已完全回滚,5 张写表计数两轮均精确回到首值
95 / 59 / 73 / 19 / 0,且既有手工演示态未被误伤。 - v1.0 业务代码零改动,符合「先报告、确认后再修」的验证轮定性;修复由另一轮次落地,本包对修复只做独立复跑取证(唯一改动的测试资产是
4-04b的断言方向,见 §5.3)。
11.2 剩余风险与未覆盖缺口(如实声明,勿读作"已通过")
| # | 缺口 | 影响 | v1.1 状态 |
|---|---|---|---|
| 1 | CT6 曾在 customer_trade_e2e_smoke.py:1250 提前 return(库内 T+2 可用份额 15000 < 申报 35000),FIFO 跨档 / 费率分项 / 超份额 400 四条断言一条未跑。连带 CT10-07 为空断言 |
✅ 已关闭(CT6 7 PASS) | |
| 2 | Redis 降级路径未验证 | 计划中的可选对照轮(停 Redis 复跑 CT11,验证 restore_flow_from_context 兜底)两轮均未执行。Redis 不可达时序号续槽能否兜底,无证据 |
⚠️ 仍开放 |
| 3 | _maintain_lots 静默失败分支未触发 |
两轮四条写链全绿(最坏情况未出现),但该函数整段 try 吞异常的结构性风险仍在 —— 只是没踩到,不代表该分支正确 | ⚠️ 仍开放 |
| 4 | 浏览器结论均在 reload() 之后成立 |
harness.mjs:176-179 的 reload 是对仓库外 BUG-1 的既有绕过。B11 已用不 reload 的真实路径反测,但其余用例首屏结论仍以 reload 后为准 |
⚠️ 仍开放 |
| 5 | 三支回归脚本仍在仓库外 | 他人 clone 拿不到脚本本体(本轮已证明会因前端改动而静默腐化)。TODO.md:197 P0 仅部分关闭 |
⚠️ 仍开放(T1 未修) |
| 6 | P0 修复只堵了方向① | infer_roles() 已不再信任入参,但 /api/auth/login 仍无 app_env 门禁、main.py:103 仍无条件挂载。本轮只证明「注入 roles 不再生效」,未证明「生产配置下非白名单 actor 一律无法登录」 |
⚠️ 仍开放 |
| 7 | P1 修复可能收窄了业务能力 | allowed_with_disclosure 被并入阻断 ⇒ R4 产品自助通道完全不可下单(原设计是「可买但需弹揭示书」)。若业务期望后者,本修复是过渡方案,需另开揭示书续接流程 |
⚠️ 需业务确认 |
| 8 | B1 既有基线不一致未排查 | core_holding 与 SUM(core_share_lot) 8 只中 3 只不符,巨型批次 LOT-SUB-TRD-20260913-68DEC409 量级异常(≈1e10÷1.3604)。两轮均以「差值断言」规避,未排查来源 |
⚠️ 仍开放 |
| 9 | CT6-08 无净值 503 分支不可达 | 库中 14 只产品全有可用净值,该错误分支无数据可触发 | ⚠️ 仍开放(数据限制) |
| 10 | CT3-05 有跨轮次顺序敏感性 |
15 分钟墙钟窗口 + jinrong_agent 永不还原 ⇒ 距上一轮 <15 min 复跑会假 FAIL。这是测试资产的设计缺陷,已在 README 与 §5.6 显式声明复跑口径 |
⚠️ 已声明(未修) |
| 11 | 被测版本是未提交工作区 | v1.0 为 merger @ 2296a4a + 21 项未提交改动;v1.1 复跑时工作区又含 P0/P1/L1 修复改动。均未对某个确定 SHA 负责 —— 提交/合并后需重跑 |
⚠️ 仍开放 |
11.3 建议排修顺序(v1.1 更新)
P0 鉴权提权→ ✅ 已修(方向①)。建议补做方向②③(login 门禁 + actor 存在性校验),成本低且能防回归P1 R4 需揭示未强制→ ✅ 已修。需业务确认:R4 是否应为「完全不可自助」(当前实现)还是「弹揭示书后可买」(原设计)—— 见缺口 7缺口 1(CT6 赎回覆盖)→ ✅ 已修(测试资产)- 缺口 10(
CT3-05顺序敏感) —— 改用「本轮 runstamp 过滤」替代 15 分钟墙钟窗口,消除跨轮次串扰 - T1 测试资产入库(
TODO.md:197P0 收口)—— 仓库外脚本已实证会静默腐化 - 缺口 2(Redis 降级路径) —— 唯一一条主口径未覆盖的功能路径
12. 修订历史
| 版本 | 日期 | 修订人 | 说明 |
|---|---|---|---|
| v1.0 | 2026-09-13 | Andrew | 初稿。覆盖 Part A 155 + Part B 48 = 203 用例;收敛 2 缺陷(P0/P1)+ 3 项其他发现;数据副作用已回滚并核对 5 表回到首值 |
| v1.1 | 2026-09-13 | Agent | 记忆追加:缺陷修复落盘(见 2026-09-13.md);未复跑,正文计数仍为 v1.0 |
| v1.2 | 2026-09-13 | Agent | 复跑取证(本节为定稿)。§5 全节刷新为 v1.1 口径:Part A 135 PASS / 0 FAIL / 1 SKIP / 25 INFO(退出码 0)· Part B 41 PASS / 0 FAIL / 0 SKIP / 7 INFO(退出码 0)。P0/P1/L1 三条追加「复跑验证」行并判 CLOSED;CT6 覆盖缺口关闭;§6/§7 的 v1.0 正文按审计惯例原样保留(未事后改写历史结论)。新增 §5.3 声明 4-04b 断言方向反转。§11.2 缺口表补入「P0 修复只堵方向①」「P1 可能收窄业务能力」「CT3-05 顺序敏感」三条。数据还原核对:5 表回到 95 / 59 / 73 / 19 / 0 |
| — | 2026-09-13 | Agent | 勘误:v1.2 起草中自曝并修正两处 — ① 曾误将 7-07c 记为「v1.1 新增对照」,实为 v1.0 已有且两轮皆 PASS;② 曾误记 v1.0 B1.6 基线为 169,298.06/6,实为 339,728.12/10(169,298.06/6 是 v1.1 的干净基线)。两处均已按 _raw/ 原始产物更正 |