Files
group_xinghuo_jinrong/docs/memory/tests/2026-09-13-customer-trade-e2e
zhanghongyu_0626 5c18164d34 fix(trade): close CT-001 P0/P1/L1 and align customer-trade E2E
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.
2026-09-13 20:04:41 +08:00
..

TEST-CT-001 · 客户线交易(申购 / 赎回 / 转换 T+1 + 对话挂单 + 交易中心)端到端包

当前结论(v1.1 复跑,2026-09-13 19:26): Part A 真实 HTTP —— 135 PASS / 0 FAIL / 1 SKIP / 25 INFO,退出码 0;Part B 真浏览器 —— 41 PASS / 0 FAIL / 0 SKIP / 7 INFO,退出码 0。 v1.0 上报的 P0(roles 注入提权)· P1(R4 需揭示在交易通道未强制)· L1(幂等键冲突静默返回原单)三条缺陷均已修复,并已用同一套用例复跑取证转绿;v1.0 最大的覆盖缺口(CT6 赎回写路径整组未执行)已关闭 —— CT6 现产出 7 PASS。

v1.0 原貌(保留对照,勿删): 155 行 · 122 PASS / 6 FAIL / 1 SKIP / 26 INFO · 退出码 1。v1.1 为 161 行 —— 增行主要来自 CT6 组由 1 条组中止占位扩为 8 条实际用例;两轮行数不可直接相减(另有少量行的增删,未逐条溯源)。 两条读数纪律(v1.0 沿用): ① INFO 不是 PASS,不得计入通过率分母;② 浏览器结论的"数字对不对"由 Part A 同名用例背书。

与前三轮的口径差异(本轮的重点):此前 E2E 只验「页面渲染出来 + 无 4xx/5xx」,页面上的数字是否正确未验证(CHAT-1~4 四条真缺陷即由此漏掉,见 TODO.md:236-239)。本轮逐条把前端渲染值与后端 raw 响应核对到分。


产物

产物 路径 位置
企业级测试日志 TEST-LOG-2026-09-13-CT-001.md 本包
客户交易 API 真实 HTTP 冒烟 scripts/dev/customer_trade_e2e_smoke.py 仓库内(本轮新建)
浏览器 E2E 共用骨架 scripts/e2e/harness.mjs 仓库内(本轮新建)
客户交易浏览器 E2E scripts/e2e/12-customer-trade.mjs 仓库内(本轮新建)
顾问前端浏览器回归(17 用例) 09-advisor-pages.mjs 仓库外 C:/Users/Windows/e2e-jinrong/
风控前端浏览器回归(21 用例) 10-risk-pages.mjs 仓库外
跨角色直达/菜单隔离(9 用例) 11-cross-role.mjs 仓库外
结果明细(原始数据) _raw/ —— v1.0 与 v1.1 两轮矩阵 + JSON + 截图 本包

TODO.md:197 的 P0「E2E 最小集进仓库」本轮部分关闭:客户线两支脚本已入库(原先一支都不在仓库里)。 三支回归脚本仍在仓库外 —— 这是刻意的(不改 web/package.json、不新增浏览器依赖),代价已由本轮实测暴露:前端登录页重写后它们集体失效(见日志 §8 T1)。

结果明细 _raw/(逐用例原始记录,未加工)

文件 轮次 内容 行数 结果
_raw/ct-api-report.md v1.0 客户交易 API 明细矩阵(修复前基线) 155 122 PASS / 6 FAIL / 1 SKIP / 26 INFO
_raw/ct-api-report-v1.1-postfix.md v1.1 同上(P0/P1/L1 修复后复跑) 161 135 PASS / 0 FAIL / 1 SKIP / 25 INFO
_raw/results-12-customer-trade.json v1.0 客户前端逐用例(修复前基线) 48 40 PASS / 0 FAIL / 1 SKIP / 7 INFO
_raw/results-12-customer-trade-v1.1-postfix.json v1.1 同上(修复后复跑) 48 41 PASS / 0 FAIL / 0 SKIP / 7 INFO
_raw/shots/ ⚠️ v1.1 11 张页面截图 — 见下方取证说明

两份 --report 均由脚本直接落盘;两份 JSON 是脚本 writeResults() 的原产物。四者均原样搬运、未加工。 ⚠️ 取证说明(如实声明):_raw/shots/ 内的 11 张截图是 v1.1 复跑时就地覆盖写入的(脚本 shots 路径为固定常量),v1.0 的截图已不可恢复**。因此截图只能佐证「修复后」状态;v1.0 的修复前结论仅有 JSON 与报告文本可查,无截图。


一眼结论

跑道 轮次 行数 PASS FAIL SKIP INFO 退出码
A 客户交易 API(真实 HTTP + 真实 MySQL) v1.0 155 122 6 1 26 1
A 同上 v1.1 161 135 0 1 25 0
B 客户前端(真浏览器 + 真实 LLM) v1.0 48 40 0 1 7 0
B 同上 v1.1 48 41 0 0 7 0
业务面 v1.0 v1.1
下单闸门(本人 / 归属顾问 / risk_demo 放行;非本人 / 非归属 403;缺 X-Agent-Type 401) ✅ 全绿 ✅ 全绿
申购放行写链(core_trade + core_share_lot + core_holding + 读侧 四链同断言) ✅ 全绿 ✅ 全绿
赎回放行 · FIFO 跨档扣减 · 费率分项 · 超份额 400 ⚠️ 未覆盖 ✅ 7 PASS(仅「无净值 503」数据不可达 → SKIP)
转换受理 202(T 日不扣份额)· 跨主体 400 · 幂等 · 撤单 owner/scope 双闸门 ✅ 全绿 ✅ 全绿(幂等冲突 409 转绿)
转换管理端点(risk_officer 专属 · latest_nav_date 与库一致 · 并发锁 409) ✅ 全绿 ✅ 全绿
读侧对账(holdings / trades / products 与库逐项一致 · 无敏感字段原文 · limit 边界) ✅ 全绿 ✅ 全绿
对话线(SSE 帧序 · 挂单不写库红线 · 序号续槽 · R4 不生成挂单) ✅ 全绿 ✅ 全绿
前端渲染值对账(总资产 Δ · 明细行文本 · 事件派发/不派发 · 错误态) ✅ 全绿 ✅ 全绿
鉴权:roles 注入提权 ❌ FAIL ×2 → P0 ✅ PASS ×2(已修复)
适当性:R4「需揭示」在交易通道未被强制 ❌ FAIL ×4 → P1 ✅ PASS ×4(已修复)

交叉印证(v1.0 成立,v1.1 仍成立):P1 在申购(CT4-01/02)与转换(CT7-07/7-07b)两条通道同一根因复现,且前端 tradeEligibility.ts、对话清单、products API 三处都已判 R4 不可自助 —— 仅交易网关这一条通道漏判,说明不是设计分歧而是单点实现缺失。


缺陷修复验证(v1.1 的取证,逐条可复跑)

编号 缺陷 复跑前实测 复跑后实测 判定
P0 /api/auth/login 信任调用方传入的 roles 2-08B 实验组 HTTP 200(FAIL,越权 token 生效);2-08C 对照组登录被拒 → 实验组 HTTP 200、JWT 实签发 roles=['advisor','risk_demo'](FAIL);2-10 写增量 Δ=4(含 2-08C 的越权写) 2-08B → 403 AUTH_403_AGENT_MISMATCH;2-08C → 实验组同样 401 Unauthorized(对照组与实验组不再有差);2-10 → Δ=3,恰等于 2-01/03/05 三笔放行写 ✅ CLOSED
P1 R4「需揭示」在交易网关被当普通放行 CT4-01/02 blocked=false;CT7-07 → 202 受理;CT7-07b Δ受理单 =1 4-01 blocked=True requires_disclosure=True match=allowed_with_disclosure;4-02 同;7-07 → blocked=True;7-07b Δ受理单=0;新增对照 7-07c(R3 → 202)通过 ✅ CLOSED
L1 同幂等键不同请求体 → 静默返回原单 CT7-05 当时是软探针 INFO(契约未定义):实测 HTTP 202 accepted=True idempotent=True qty=1000.00 —— 参数被吞、静默复用旧单,调用方无从察觉 7-05 升为硬断言 PASS → 409 IDEMPOTENCY_CONFLICT ✅ CLOSED(契约由此冻结;修复的连带收益)
CT6 赎回写路径整组未执行(覆盖缺口,非应用缺陷) CT6 仅 1 行 = 组中止 SKIP CT6 7 PASS / 1 SKIP:6-02 金额复算 44922.38 与库内一致 · 6-03 FIFO 跨档(LOT-…-01 扣 30000.0000 清档 + LOT-…-02 扣 150.0000)· 6-04 快照同步 · 6-06 超份额 400 · 6-07 缺 qty 422 ✅ CLOSED

另:4-04b 断言已反转(本轮自曝的测试资产腐化) —— v1.0 的 4-04b 写的是「R4 放行则确实写库」,即把修复前的缺陷行为固化成了期望值(同 sandbox_risk_test.py:325-326 的 T1 腐化模式:断言跟着 bug 走,bug 一修就变假红)。已改为「R4 被阻断 ⇒ core_trade 零写入」,与 4-04(R5)合起来构成两类阻断的零写入证据。这是本包内唯一被改动的测试资产,改动理由已写入脚本注释。


复现三连

# 0) 前置:uvicorn 8000 + Vite 5173 + Redis 6380(Redis 必须起,序号续槽走真实抽槽路径)
curl -s http://127.0.0.1:8000/api/ready        # 期望 ok=true, redis=true

# 1) 类型闸门 + 单测基线(比浏览器便宜得多,先跑)
cd web && npm run build && cd ..               # 期望退出码 0
python -m pytest tests/test_trade_gateway.py tests/test_trade_flow_service.py \
                 tests/test_trade_action_service.py tests/test_convert_confirm.py -q   # 期望 76 passed

# 2) 快照(跑前;跑后还原,见下)
MD=/c/tools/mysql/mysql-8.0.27-winx64/bin
$MD/mysqldump -h127.0.0.1 -uroot -p jinrong_core \
  core_trade core_holding core_share_lot core_convert_request core_convert_lot_detail \
  --single-transaction --add-drop-table > /tmp/ct-snapshot.sql

# 3) Part A 真实 HTTP E2E(退出码 0 全过 / 1 有 FAIL / 2 前置失败)
python scripts/dev/customer_trade_e2e_smoke.py --report /tmp/ct-api.md
python scripts/dev/customer_trade_e2e_smoke.py --only CT2 --only CT4   # 单组复跑

# 4) Part B 真浏览器 E2E(须 Vite 5173;app 无 CORS,浏览器不能直连 8000)
node scripts/e2e/12-customer-trade.mjs        # Playwright 由 PW_HOME 解析,不装依赖

# 5) Part C 回归复跑(三支共用同一 5173,必须串行 —— 脚本内切角色会互相顶掉 localStorage)
cd C:/Users/Windows/e2e-jinrong && node 09-advisor-pages.mjs && node 10-risk-pages.mjs && node 11-cross-role.mjs
python scripts/dev/risk_e2e_smoke.py --only R6      # 借风控视角反向印证同一批 convert 语义

# 6) 还原并核对(用 CT12-01 的计数对照;v1.1 实测 5 表精确回到首值)
$MD/mysql -h127.0.0.1 -uroot -p jinrong_core < /tmp/ct-snapshot.sql

⚠️ 复跑前必读:CT3-05 有跨轮次顺序敏感性。 它的查询是 audit_log.created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE)(墙钟窗口),而 jinrong_agent 按审计语义永不还原(见「数据副作用」)。因此上一轮跑完 15 分钟内直接复跑,3-05 会假 FAIL(读到上一轮的 suitability_blocked 留痕)。正确做法:距上一轮 ≥15 分钟后,从已还原的基线单跑一次。 v1.1 的 3-05(0 条阻断留痕/PASS)即按此口径取得 —— 本轮曾实测到 3 条残留并等待窗口自然老化后重跑。

v1.1 仅剩 1 条 SKIP —— 属数据不可达,非应用缺陷

SKIP 性质 说明
A/CT6-08 无可用净值 → 503 NAV_NOT_READY 数据不可达(非缺陷) 库中 14 只产品全部有可用净值,该错误分支本轮不可达。与 v1.0 的 CT6 SKIP 性质完全不同 —— v1.0 那条是整组中止,这条是组内单例的自然跳过

v1.0 的 B/B3.3 SKIP 在 v1.1 已转绿:v1.1 的 B2.0 挑到了真正未持有的自助品 PROD-110023,故 B3.3(持仓只数 6 → 7)成立。


未覆盖 / 已声明的缺口

  1. ⚠️ 赎回写路径整组未覆盖(本轮最大缺口) → v1.1 已关闭(CT6 组 7 PASS)。 原缺口存档:CT6 组曾在 customer_trade_e2e_smoke.py:1250 提前 return —— t2_lots(CUST-9527, PROD-005827) 的 T+2 可用批次合计仅 15000.0000,低于用例写死的申报 35000。修复方向即当时所写:把申报量改为由库内事实推导(现为 derive_redeem_qty(lots, total_avail):优先取一次 FIFO 跨两批的咬合量,退化为 round2(total_avail * 0.3))。连带影响已消除:v1.0 中 CT10-07 赎回后读侧与快照一致 是空断言,v1.1 中 CT6 真发生赎回,该断言不再是空转。

  2. Redis 容灾降级路径未验证 —— 计划里的「可选对照轮」(停 Redis 复跑 CT11 组,验证 restore_flow_from_context 兜底)两轮均未执行。主口径(Redis 起)已全绿,但降级路径的正确性无证据。

  3. _maintain_lots 静默失败路径未被触发 —— 两轮四条写链全部同断言通过(R1 风险点的最坏情况未出现),但该函数整段 try 吞异常的结构性风险仍在,本轮只是没踩到。

  4. 浏览器结论均在 reload() 之后成立 —— harness.mjs:176-179 的 reload 是对仓库外 BUG-1 的既有绕过手段。B11.1 已用不 reload 的真实登录路径反测(无 ErrorBoundary、三 Tab 就位),但其余用例的首屏结论仍以 reload 后为准。

  5. 三支回归脚本仍在仓库外,他人 clone 拿不到脚本本体(结论可查阅,不可重跑)。

  6. B7 取样点重合 → v1.1 已消除:v1.1 的「未持有品」为 PROD-WMG001(申购 enabled),与 R4 品不再重合。v1.0 的成因存档:可自助未持有品已被多轮 E2E 买空,当时「未持有」与「R4」恰为同一只 PROD-003095,两条结论落在同一点上。

⚠️ v1.0 的 Part B 资产基线被 Part A 污染(v1.1 已修正口径): v1.0 B1.6 基线实测为 339,728.12 / 10 只,B3.2 的对照组合法地写在其上(339,728.12 → 352,073.79)—— 数字自洽,但基线本身是脏的:v1.0 的 Part B 在 Part A 之后、还原之前执行,那 10 只持仓里含着 Part A 的未还原写入。 v1.1 的 Part B 从已还原的基线起跑,B1.6 = 169,298.06 / 6 只,B3.2 = 169,298.06 → 181,643.73 —— 这才是真实资产面。 连带后果:v1.0 B2.0 因「可自助未持有品已被买空」走了退化分支(取已持有的 PROD-510300),导致 B3.3 判 SKIP、B7.0 的「未持有品」与「R4 品」撞成同一只 PROD-003095。v1.1 基线干净后,B2.0 取到真正未持有的 PROD-110023,B3.3 转 PASS、B7.0 分离为 PROD-WMG001。 举此例是因为它正是本包反复强调的那条口径:同一批表被多条用例串行读写,前一条污染后一条的前置 —— 连「测试跑道之间的执行顺序」也算在内。


附:本轮发现(v1.0 上报,v1.1 复核)

编号 级别 一句话 落点 v1.1 状态
P0 高 /api/auth/login 无条件信任调用方传入的 roles,可自造任意角色 token app/gateway/jwt_service.py:118-120 · app/api/auth.py ✅ 已修复(infer_roles 改为仅查 DEFAULT_ROLES_BY_ACTOR 白名单)
P1 中 R4「需揭示」产品在交易网关被当普通放行,申购与转换两通道同时漏判 app/gateway/trade_gateway.py:534 ✅ 已修复(新增 suitability.simulate_self_service_blocked,网关与 convert 服务收敛同口径)
L1 低 幂等键相同但请求体不同 → 静默返回原单,调用方无从察觉 app/service/convert/* ✅ 已修复(→ 409 IDEMPOTENCY_CONFLICT)
T1 — 前端登录页重写令仓库外三支回归脚本集体失效(本轮已临时修补) scripts/e2e/harness.mjs 注释 + 日志 §8 ⚠️ 未修(脚本仍在仓库外,腐化风险持续)
B1 — (既有,非本轮引入)core_holding 快照与 SUM(core_share_lot) 不一致,8 只产品中 3 只对不上 见日志 §10 ⚠️ 未修(两轮均复现)

逐条根因见 TEST-LOG-2026-09-13-CT-001.md §6。