Files
group_xinghuo_jinrong/docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md
T
zhanghongyu_0626 f856ab4aa6 fix(tests): Resolve pytest session binding issues and update test cases
- Updated test files to import `AgentSessionLocal` from `advisor_db` instead of directly, preventing session binding to the real database during tests.
- Fixed 6 test cases to use the new login token utility, ensuring consistency across authentication methods.
- Adjusted customer risk codes in `test_convert_confirm.py` to reflect changes in customer classification (C3 to C4).
- Verified that changes resulted in zero database pollution during test runs, maintaining integrity of the testing environment.
- Documented findings and updates in the relevant test logs and memory files, ensuring clarity on the current state of tests and defects.
2026-09-13 23:21:34 +08:00

40 KiB
Raw Blame History

端到端覆盖缺口补测轮 · 三条线并跑(2026-09-13)

方法:superpowers 的 verification-before-completion(先取证再下结论)· systematic-debugging(先定根因再提修法)· dispatching-parallel-agents(独立域并行)· writing-plans(本计划)。 执行时把本计划归档副本写入 docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md。

📌 修复轮跟进(2026-09-13 晚): 本计划原口径「缺陷只报告不修」,用户随后拍板「开始修吧」⇒ 对可修项开修复轮,已落地:F-β 选 A 已执行(test_convert_confirm.py C3→C4,四文件 77/0)· F-ε 已修(10 个 test_sprint*.py 模块属性访问 + skip 写真库 subprocess,零污染)· 6 个 sprint2 陈旧登录契约已迁移 · P0 mock 登录门禁「本轮不动,只记录」。详见 docs/memory/tests/2026-09-13-crosscut-e2e/。

Context

为什么做这一轮: 上一轮(TEST-2026-09-13-CT-001)刚把客户线交易做成全量端到端——真浏览器 + 真 HTTP + 真库对账,Part A 135 PASS / 0 FAIL / 1 SKIP / 25 INFO(退出码 0)、Part B 41 PASS / 0 FAIL / 0 SKIP / 7 INFO(退出码 0)。

本轮起因是一个问题:「还有哪块没有端到端跑过?」

盘点已完成(结论见下节),答案是:三条业务线尚未闭环(问数线最弱——12 条路由零 HTTP E2E)、前端大量浅覆盖、以及一批要收进仓库的脚本自身是坏的(恒真断言 / 路由写错 / 选择器过期)。本轮按拍板口径直接开跑补齐,缺陷只报告不修,修复另开轮次。

预期产出: 四个可复跑的独立证据包 + 逐条缺陷清单 + 一份测试基建修复台账(与业务缺陷分开记账)。


一、盘点结论(本轮的事实基础)

以下全部为实测,非推断。带 ⚠️ 的是我复核后修正过 Explore agent 结论的地方。

1.1 后端:66 路径 / 71 个 path+method 处理器

挂载点 app/main.py:103-125(23 个 router)+ :128/:172(app 级)。

零 E2E 覆盖(★ = 连单测都没有):

路由 证据
POST /api/customers/{id}/threshold-check ★ 前端按钮 web/src/pages/customer/CustomerHoldingsPage.tsx:21-27;全仓 e2e grep 零命中;tests/test_main.py:104 仅路径字符串断言
GET /api/analyst/ops/metrics ★ 前端无任何调用方;脚本零命中
POST /api/analyst/interpret 前端有 web/src/api/analyst.ts:84 但无页面调用(AnalystQueryPage.tsx:105 只传 interpret:false)
POST /api/analyst/escalate 仅 UI 按钮 AnalystQueryPage.tsx:361,无脚本点过
GET /api/analyst/query/{trace_id}/sample 仅 UI 按钮 AnalystQueryPage.tsx:442
POST /api/analyst/analyze 仅 UI 弹窗按钮 AnalystQueryPage.tsx:388
POST /api/analyst/assets 仅 UI 表单 AnalystAssetsPage.tsx:15-23;04-analyst-market-advisor.mjs:40 只开页未提交
POST /api/analyst/assets/{kind}/{asset_id}/publish 零命中
POST /api/analyst/dict/ambiguity-check 零命中
GET /api/analyst/dashboard · GET /api/analyst/template-prompts 仅"开页时请求发出",无任何断言

仅单测覆盖: GET /api/products/{product_id}(前端无人调用)· GET /api/staff/me(前端零调用)· POST /api/chat/sessions/close-all(按钮 ChatPanel.tsx:72 无人点)· POST /api/chat/sessions/{id}/close(弹窗 ChatPanel.tsx:176 无人点)。

已充分覆盖: 风控线 4 条路由全覆盖;顾问线 /api/advisor-agent/* 写操作由 advisor_e2e_smoke.py 全覆盖;客户线交易全套由 CT-001 覆盖。 顾问线但前端无调用方(仅 smoke 一条命脉):POST /advisor-agent/guard/check · POST /advisor-agent/copy/track · GET .../dashboard/ping · GET .../allocation/ping。

非 HTTP 入口未测: scripts/cron/escalation_scan.py · scripts/cron/agent_behavior_scan.py(服务层有单测,CLI 无)· scripts/demo/push_threshold_alerts.py · scripts/demo/subscribe_alerts.py。 不存在: MCP、WebSocket、工单实体、app/api/admin.py 与 app/api/knowledge.py(各仅 1 行 docstring,零路由)。

已失效脚本(会 404,不能当覆盖用): scripts/demo/run_demo_smoke.ps1(仍在打已不存在的 /api/v1/*)· scripts/eval/evaluate_agent.py:49,59(同)。

1.2 前端:30 路由 / 27 组件

从未访问: /register · /app/home · /app。 浅覆盖: AnalystAssetsPage(表单从未提交)· AnalystQueryPage(数字从未对账)· AdvisorClientsDashboard(AUM 从未对账)· AdvisorCustomersPage(代客下单链接从未点)· 所有聊天会话侧栏 · 面包屑 · 登出 · 页面级响应式。

⚠️ 两个数字待 Phase 0 复核,本计划不当作事实:①深/浅的具体分档数(Explore agent 报 12 深/13 浅/3 未访问,但 12+13+3=28 ≠ 30,分档口径本身对不上,须以 web/src/routes/menus.tsx 实际路由表重新分档);②"27 组件"与 30 路由的关系未核。/register 零覆盖这一条是独立的、可信的(有独立证据:web/src/pages/login/RegisterPage.tsx 新增且无任何脚本访问),不依赖上述分档。

1.3 ⚠️ 我复核出的新发现(Explore agent 报错了,我重新核过)

agent 原报:「app/graph/agent_graph.py:158 与 app/service/agent_tools.py 7 处函数签名不匹配,wiring 就 500」。

复核结果:这条描述错误**,但底下的问题真实存在且更严重。**

  • 路径就错了:两个文件都在 app/service/,不存在 app/graph/。
  • build_advisor_tools 的签名与调用点完全一致(agent_graph.py:163-168 ↔ agent_tools.py:178-183),无签名不匹配。
  • 那"7 处"行号(70,113,118,123,128,150,170)全是函数体内的方法调用,不是函数定义。
  • 真正的问题在更下一层——那些工具调用的 service 方法有三个不存在。我逐个核对真实 service 源码:
# 工具 工具内调用 真实 API 判定
1 create_query_holdings_tool core_repo.get_customer_l0 · core_repo.list_holdings core_ro.py:63 ✅ · core_ro.py:282 ✅ OK
2 create_query_fund_nav_tool core_repo.list_product_nav_history(product_id, limit=) core_ro.py:889 ✅ 签名一致 OK
3 create_manage_kyc_tool kyc_service.chat(session_id=, user_input=) 真名 chat_session(kyc_session_service.py:140) ❌ AttributeError
3' 同上 kyc_service.get_session(session_id=) :133 需 keyword-only auth: AdvisorAuthContext ❌ TypeError
4 create_search_templates_tool template_service.search(query=, limit=) TemplateService 无 search(只有 try_render/list_published_prompts/reload);真搜索在 script_template_service.py ❌ AttributeError
5 create_compliance_check_tool compliance_service.check(text=, scene=) 真名 check_text(payload: ComplianceCheckRequest, *, trace_id, advisor_id)(compliance_check_service.py:38)—— 连调用形态都不同 ❌ TypeError

即 build_advisor_tools 5 个工具里 3 个(工具 3/4/5)接错。

为什么一直没被发现: build_advisor_graph 是休眠代码——全仓唯一调用方是 tests/test_step11_agent_graph.py:113,而它在 :114-117 给四个 service 全注入 Mock()。Mock() 对任意属性访问都返回可调用对象,所以 kyc_service.chat(...)、template_service.search(...)、compliance_service.check(...) 在测试里全部"通过"。

这正是盘点里"19 个 test_sprint*.py 伪造全部 Core 数据源 → 结构上不可能发现接缝不匹配"的具体实例。本轮 AD8 组要把它从"推断"变成"取证"。

1.4 测试资产腐化(已定位行号)

文件(均在仓库外 C:/Users/Windows/e2e-jinrong/,见 1.5) 问题
03-risk.mjs:23 ok(..., true, ...) 恒真断言 —— 永远 PASS,不构成证据
06-chat.mjs:12 构造了不存在的路由 /app/analyst/chat(正确为 /app/analytics/chat)
08-edge.mjs:15,41 选择器过期,预计在 :42 抛异常
11-cross-role.mjs:104 C1.6 前提已过期
run_demo_smoke.ps1 已失效(打 /api/v1/*)
docs/演示文档/功能演示版验收标准.md 多条质量门槛陈旧:/api/v1/ping 路由已不存在;alembic heads 写的是旧版本号;run_demo_smoke.ps1 连续执行通过这条已无法成立
docs/答辩/DEMO-功能测试流程.md D2/D3 一号两义(第 64-65 行是用例编号、第 79/89 行是缺陷编号,详见 4.2 AD7)。答辩稿里的编号歧义,须记入横切包 XC

1.5 ⚠️ 浏览器脚本的真实分布(修正)

scripts/e2e/ 目前只有两个文件:12-customer-trade.mjs(37293 B)+ harness.mjs(9301 B)+ shots/。 01–11 全部在仓库外 C:/Users/Windows/e2e-jinrong/,另有 FINDINGS.md、results-*.json、一批 dbg*.mjs。

两个 harness.mjs 已经分叉,不是同一份:

仓库内 scripts/e2e/harness.mjs 仓库外 e2e-jinrong/harness.mjs
大小 9301 B 6049 B
来源 上一轮 CT-001 新建 09-10 那轮,harness.mjs.bak-pre-20260913 是改前备份

→ 09/10/11 收回仓库时必须改指向:把 require 从仓库外 harness 改为 scripts/e2e/harness.mjs,并逐一核对它们用到的原语(login/clearAuth/waitRows/issues)在仓库内版本里是否存在、语义是否一致。这是收回动作的真实成本,不要当成 cp。

可得基线证据: e2e-jinrong/results-09-advisor.json(18:41)· results-10-risk.json(18:42)· results-11-cross-role.json(18:38)—— 是上一轮 Part C 跑出的产物,可作本轮对照。

1.6 工程基建缺口

无 CI · 无覆盖率工具 · 无 Makefile / 统一测试入口。tests/ 下 19 个 test_sprint*.py 共 65 条失败。→ 只记录。


二 · 补 · 复核修正(设计 agent 提出、我已逐条实测确认,以本节为准)

# 我原先写的 实测 修正
C-1 customer_trade_e2e_smoke.py「含两个头构造器 + SSE 逐帧 + **--snapshot/--restore」 没有 --snapshot/--restore。scripts/dev/customer_trade_e2e_smoke.py:2175-2178 只有 --base-url / --report / --only / --timeout。快照/还原在上一轮是外部 shell 步骤,从来不是脚本机制 新脚本不继承不存在的开关;快照/还原继续用外部 mysqldump/mysql(与上轮口径一致)。本轮也不自造这组开关
C-2 三支 smoke「同构」,INFO 纪律通用 risk_e2e_smoke.py:60 只有 PASS, FAIL, SKIP = "PASS","FAIL","SKIP"——没有 INFO;advisor_e2e_smoke.py 同 「INFO 不是 PASS」是 customer_trade_e2e_smoke.py 独有的。新脚本照抄 customer_trade 的原语,不要照抄 risk
C-3 F-06 要「改造脚本把截图路径参数化」 scripts/e2e/harness.mjs:39 已是 export const SHOT_DIR = process.env.E2E_SHOT_DIR || path.join(__dirname,'shots') F-06 降级为「用 env 变量」,零代码改动。每支脚本跑时给不同 E2E_SHOT_DIR 即可
C-4 「扩充 advisor_e2e_smoke.py / risk_e2e_smoke.py 新增分组」 这两支是 Part C 的回归基线,其价值是「退出码仍为 0」。而 AD8 是预期 FAIL 的取证组 → 加进去会让退出码变 1,与真回归不可区分 改为新建独立脚本(见下)。两支既有脚本一个字都不动

脚本落点修正(取代原「扩充」方案):

脚本 覆盖
scripts/dev/analyst_e2e_smoke.py(新) AN1–AN12,问数线 12 个 handler
scripts/dev/gap_routes_e2e_smoke.py(新) GX1–GX5,不属任何单线的孤立路由(★threshold-check、staff/me、products/{id}、chat 会话侧栏)+ 原 XC1/XC4
scripts/dev/advisor_gap_e2e_smoke.py(新) AD7–AD10,含 AD8 接缝取证(预期 FAIL,单独成脚本正是为了不污染基线)
scripts/dev/risk_gap_e2e_smoke.py(新) RK7–RK8
扩充 advisor_e2e_smoke.py / risk_e2e_smoke.py 取消——原样保留作 Part C 基线

关于 AD8 的一处不同意见(记录在案): 设计 agent 实测 build_advisor_graph(...Mock()) → graph ok: True True,据此判定"无签名不匹配、只是孤儿代码、不是缺陷"。这与我 1.3 的结论不矛盾且不充分——它用 Mock() 验证,而 Mock() 正是遮蔽该问题的原因(对任意属性访问都返回可调用对象)。我已读真实 service 源码确认 3 处绑定错误(chat/search/check 不存在;get_session 缺 keyword-only auth)。结论:agent_graph.py 整支确为孤儿代码(全仓仅 tests/test_step11_agent_graph.py 引用)→ 严重度降级为"潜在缺陷 + mock 遮蔽",但 AD8 取证保留,因为"孤儿代码里 3/5 工具接错 + 测试给出假绿"本身就是必须记录的事实。报告里按潜在缺陷列,不列为 P0/P1。


二、已拍板口径(七项,不可动摇)

# 项 选择
1 本轮产出边界 盘点 + 直接开跑补齐
2 发现缺陷的处置 只报告不修,修复另开轮次
3 仓库外 09/10/11 本轮收回仓库
4 数据副作用 mysqldump 快照 + 跑后还原
5 腐化测试资产的处置 修,但单独记账(测试基建台账,与业务缺陷分开)
6 覆盖范围 三条线全补
7 报告包粒度 一线一包

二 · 补2 · Phase 0 实测结果(2026-09-13 20:12~20:20,已落盘,以此为基线)

门槛 文档 / 上一轮声称 本轮实测 判定
/api/ready — ok=true degraded=false redis=true,且新增 chat_close_all/chat_sessions/analyst_chat/products_nav_history 四项检查 ✅
openapi 66 路径 66 路径 / 71 handler ✅ 与计划一致
Vite 5173 — HTTP 200 ✅
npm run build 退出码 0 退出码 0(47.18s) ✅
npm run test 27 passed 27 passed (8 files) ✅
pytest -q --ignore-glob=test_sprint*.py 896 passed ❌ 1232 passed / 102 failed / 21 errors / 9 skipped ⚠️ 发现 F-α
交易四文件单测 76 passed ❌ 49 passed / 28 failed ⚠️ 发现 F-β
core 5 表首值 95/59/73/19/0 95/59/74/19/0(core_share_lot=74) ⚠️ 发现 F-γ
core_trade 非测试前缀行 — 95 行中 0 行带测试前缀 ✅ 还原安全

快照: /d/tmp/e2e-gap/snapshot.sql(45658 B,2026-09-13 20:13)—— 覆盖 core_trade/core_holding/core_share_lot/core_convert_request/core_convert_lot_detail。

Phase 0 三项新发现(均属"测试基建/文档",非业务缺陷)

⚠️ /tmp 两义性再修正:Git Bash 的 /tmp ≠ Python 的 /tmp。实测 Python 的 /tmp 解析为 <当前盘符>:\tmp(本轮 cwd 在 D 盘 → D:\tmp),不是上一轮记录的 C:\tmp。本轮统一用 /d/tmp/e2e-gap(bash)/ D:/tmp/e2e-gap(Python),彻底绕开。

F-α — 验收文档的门槛数字与实测严重不符。 docs/答辩/DEMO-功能测试流程.md A3 称 896 passed;实测 1232 passed / 102 failed / 21 errors。 根因已定位(非 DB 连通性问题):测试跑在 SQLite 内存库 + conftest.py 自建假 DDL 上,而假 DDL 与 fixture 自身的插入数据不一致——例:tests/test_platform_api.py 全部 21 条 error 是 sqlalchemy.exc.OperationalError: table core_holding has no column named quantity(fixture INSERT 了 quantity,假 DDL 未声明该列)。 → 只报告。本轮完成判据中的"896 不退步"改为"不退步于实测基线 1232 passed / 102 failed / 21 errors"。

F-β — 上一轮的 P1 修复打断了 29 条单测,且上一轮报告"76 passed 不退步"已不成立。(本轮最高价值发现) 证据链完整:

  1. git show 5c18164 -- app/service/convert/convert_service.py 显示该 commit 把 if suit.blocked: → if suit.blocked or simulate_self_service_blocked(suit):(就是上一轮的 P1 修复)。
  2. 该 commit 同步更新了 tests/test_convert_accept.py(43 行),但漏改了 tests/test_convert_confirm.py。
  3. 后者 tests/test_convert_confirm.py:42 仍写着 CUST = "CUST-CC" # C3 客户 → 转入 R4 = 放行——固化的是"R4 转入=放行(需揭示)"这条旧语义。
  4. 新语义下该路径走入阻塞分支;而 convert_service.py:522-535 的阻塞返回体不含 forced_full_transfer 键 → tests/test_convert_confirm.py:1013 的 accepted["forced_full_transfer"] KeyError → 28 条失败。
  5. 另有 tests/test_convert_accept.py::test_accept_uk_idem_race_falls_back_to_idempotent 1 条失败(疑与该 commit 的 409 幂等改动相关)。 → 这正好实证了上一轮 MEMORY.md 里"⚠️ 复跑新发现(需拍板)"的第 ② 条:「P1 修复可能收窄了业务能力」。只报告不修,但它是一条明确的待拍板项(业务上 R4 到底该"放行+揭示"还是"直接阻断")。

F-γ — core_share_lot 首值为 74,不是上一轮记录的 73。 上一轮 README 载明还原目标是 95/59/73/19/0;本轮跑前实测为 95/59/74/19/0(多 1 行)。 → 不影响本轮(以本轮快照为准),但说明两轮之间基线发生过变化(疑为手工演示动作)。还原目标 = 本轮快照的 95/59/74/19/0。


三、交付物:四个包

用户定的是"一线一包"。三条业务线各一包;横切件(跨角色 / 前端全局 / 腐化资产台账 / 基建缺口)单独成第四包——理由:跨角色用例跨三条线、腐化资产是测试基建而非任何一条线的业务缺陷,塞进任一业务线包都会污染那个包的结论边界。

包 路径 日志编号 覆盖
问数线 docs/memory/tests/2026-09-13-analyst-e2e/ TEST-LOG-2026-09-13-AN-001.md 12 条路由全覆盖(本轮重点)
理财师线 docs/memory/tests/2026-09-13-advisor-e2e/ TEST-LOG-2026-09-13-AD-001.md KYC 闭环 + 接缝取证 + 代客下单
风控线 docs/memory/tests/2026-09-13-risk-e2e/ TEST-LOG-2026-09-13-RK-001.md 浏览器深挖 + 恒真修复后复跑
横切 docs/memory/tests/2026-09-13-crosscut-e2e/ TEST-LOG-2026-09-13-XC-001.md 跨角色 / 前端全局 / 资产台账 / 基建缺口

每包结构照 docs/memory/tests/_TEMPLATE.md(8 节):README.md(结论一行 + 产物表 + 复现三连)+ TEST-LOG-*.md + _raw/。 企业级范例: docs/memory/tests/2026-09-10-customer-1b-r1/TEST-LOG-2026-09-10-CS-001.md。

截图路径坑(必处理): _raw/shots/ 在脚本里是固定常量路径,重跑就地覆盖。上一轮因此丢了 v1.0 修复前截图,已记录为证据链缺口。本轮四个包各跑各的,必须先把各脚本的截图输出路径参数化(或在脚本内按包名拼路径),否则四个包会互相顶掉。


四、Part A 用例矩阵(真实 HTTP)

命名 AN<n>-<mm> / AD<n>-<mm> / RK<n>-<mm>。取证列:HTTP(状态码+响应字段)· DB(只读查库交叉印证)· SSE(逐帧)· DOM(浏览器)。 所有写侧断言一律写成「前后差值」(见第七节),矩阵不再重复标注。

4.1 问数线 AN —— 新建 scripts/dev/analyst_e2e_smoke.py(约 40 条)

骨架照抄 scripts/dev/customer_trade_e2e_smoke.py(最新最全),继承其 preflight/Result/db_query/退出码三级约定。

AN1 前置与契约冻结(4) /health 可达 · /api/ready 通过(checks.redis 记录但不据此判前置失败)· openapi 必需路由自检:12 条 analyst 路由逐一存在 + 记录路径总数基线 66(缺任一 → 退出码 2)· 五角色登录签发 token(customer/advisor/analyst/risk_officer/ops,账号取自 web/src/config/defenseAccounts.ts)· db_query 加载基线写入 ctx.state(期望值由库中事实推导,不写死)

AN2 鉴权与数据域(6) —— 走 app/api/analyst_auth_adapter.py:assert_analyst_query_access 这是本项目第三条鉴权路径(前两条是 get_auth_context 与 get_platform_auth_context),五角色 → 五数据域映射:customer→self · advisor→assigned · analyst→full · risk_officer→risk · ops→aggregate。 customer(带 customer_id)→ self · customer 无 customer_id → 403 AUTH_403_ROLE · advisor→assigned · analyst→full · risk_officer→risk · 无 token → 401。 跨域越权探针(本组关键):customer 身份问全量聚合问题(如"全部客户数")→ 应被 sql_guard 按 self 域拦下 → 记录实测,不预设通过。

AN3 chat 主链路(4) —— POST /api/analyst/chat 单问返回 status/SQL/rows/answer 四段齐全 · SQL 只读红线:断言返回 SQL 不含 INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE(对应红线「数据分析:仅 SELECT」)· 不处置预警红线:调 chat 前后 risk_alert 计数零增量(对应红线「不处置预警」)· 空问题/未知问题 → 记录实测

AN4 interpret(3) · AN5 analyze(3) · AN6 query/{trace_id}/sample(3) · AN7 escalate(3) —— 四条零覆盖路由,各含正常路径 + 鉴权负例 + 字段面枚举

AN8 template-prompts + dashboard(4) —— 补齐"仅开页无断言"的空白;template-prompts 断言按角色过滤(list_published_prompts(roles))

AN9 assets CRUD + publish(6) —— 零覆盖;含 POST /assets → GET /assets → POST /assets/{kind}/{asset_id}/publish 全链 + 鉴权负例 + 未知 kind 负例

AN10 dict/ambiguity-check(3) —— 零覆盖,含命中/不命中两向

AN11 ops/metrics(2) —— 零覆盖,且前端无任何调用方(记录这一事实:接口无消费方)

AN12 缓存与模板 Tag(3) —— 对应 docs/答辩/DEMO-功能测试流程.md 的 D1/D2(模板 Tag / 缓存 Tag)

4.2 理财师线 AD —— 扩充 scripts/dev/advisor_e2e_smoke.py 新增分组(约 20 条)

边界:只新增分组,绝不动既有断言 —— 它是回归基线,改了会让 FAIL→PASS 的对照失效。

AD7 KYC 全链路复现(6) —— ①复现 GET /api/advisor-agent/kyc/sessions/{id} 的 500 ②created_at 缺字段 ③complete 状态机 ④未授权 session_id 越权探针 ⑤会话不存在 → 404 ⑥ADV-001 报告余下的 3 条 FAIL 逐条复跑,判定真修复 / 仍开放 / 已失效三态

⚠️ 编号陷阱(本轮新发现的一条文档缺陷):docs/答辩/DEMO-功能测试流程.md 里 D2/D3 一号两义—— 第 64-65 行是问数线用例编号(D2 = 缓存 Tag、D3 = interpret), 第 79 行(E5 行)与第 89 行(F2 行)却是缺陷编号("D2 500 已知"、"已知 3 FAIL(D2/D3)")。 同一份答辩稿里同一个符号指两件事。本轮报告一律不使用裸 D2/D3,改用「问数线 D2」或「ADV 缺陷-2」这类带前缀写法;并把该文档缺陷记入横切包 XC。

AD8 接缝探针(5)—— 本轮最高价值的新增组,把 1.3 的发现从"读代码推断"变成"运行取证" 用真实 service 实例(而非 Mock())构造 build_advisor_tools 的 5 个工具,逐一 invoke:

用例 工具 期望(本轮取证目标)
AD8-01 query_customer_holdings 正常返回(对照组,证明探针方法本身有效)
AD8-02 query_fund_nav 正常返回(对照组)
AD8-03 manage_kyc_session(action="chat") 取证 AttributeError: chat(真名 chat_session)
AD8-04 manage_kyc_session(action="get") 取证 TypeError(缺 keyword-only auth)
AD8-05 search_templates / compliance_check 取证 AttributeError: search / TypeError: check

两个对照组是刻意的:没有 AD8-01/02 就无法排除"探针写错了"这一解释。预期本组多数 FAIL —— 这是取证,不是回归。

AD9 无前端调用方的端点(4) —— POST /advisor-agent/guard/check · POST /advisor-agent/copy/track · GET .../dashboard/ping · GET .../allocation/ping:记录"接口能用但无消费方"这一事实

AD10 代客下单链路(约 6) —— 对应前端浅覆盖点 AdvisorCustomersPage 的"代客下单"链接从未被点;后端走 risk_e2e_smoke.py 的 R6 组口径作交叉印证

4.3 风控线 RK —— 扩充 scripts/dev/risk_e2e_smoke.py 新增分组(约 12 条)

风控线后端 4 条路由已全覆盖(risk_e2e_smoke.py:245-457 覆盖 alerts/suitability/aml + 处置状态机)。本线缺口不在后端而在浏览器与资产。

RK7 后端补漏(4) —— 只补盘点查出的空白:/api/risk/suitability/check 与 POST /api/customers/{id}/threshold-check 的语义交叉(后者是本项目★级零覆盖路由,且前端有按钮);risk_alerts 分页边界;处置状态机非法迁移负例

RK8 浏览器复跑对照(8) —— 修完 03-risk.mjs:23 恒真断言后,用 10-risk-pages.mjs 重跑并逐条对照 e2e-jinrong/results-10-risk.json:修复前 PASS 但修复后 FAIL 的条目,正是原来被恒真断言掩盖的真缺陷。这是本线最有价值的一步。

4.4 横切 XC —— 新建 scripts/dev/crosscut_e2e_smoke.py + 浏览器(约 20 条)

XC1 跨角色 F1–F3(6) —— 收复 11-cross-role.mjs 并修 :104 的 C1.6 过期前提;对应 DEMO-功能测试流程.md F1/F2/F3 XC2 前端从未访问路由(4) —— /register(注册链路零覆盖,含表单校验与重复注册负例)· /app/home · /app XC3 前端浅覆盖补齐(5) —— 登出 · 面包屑 · 聊天会话侧栏 · 切换会话 · 响应式(07-responsive-a11y.mjs) XC4 仅单测路由的 E2E 补齐(3) —— GET /api/products/{product_id} · GET /api/staff/me · POST /api/chat/sessions/close-all(注意二者前端无人调用,须用 HTTP 直连取证) XC5 腐化资产修复台账(3) —— 修复前后对照(见第六节);外加 docs/答辩/DEMO-功能测试流程.md 的 D2/D3 编号歧义与 docs/演示文档/功能演示版验收标准.md 的陈旧门槛(文档类发现,同样只报告不修)


五、Part B 浏览器 + Part C 回归

Part B(真浏览器) —— Playwright 刻意不装进 web/package.json,从 C:/Users/Windows/e2e-jinrong/node_modules 经 PW_HOME + createRequire 解析,不动 web/package.json。 顺序:只读导航先行 → 再写侧 → 对话类(依赖真实 LLM,最脆)放最后。每包只跑本包相关脚本,串行——脚本内 clearAuth 切角色,并发跑会互相顶掉 localStorage。

Part C(回归) —— 09→10→11 三支收复并改指向仓库内 harness 后复跑;再跑既有的四文件交易单测基线(76 passed,不得退步)与 npm run build 类型闸门。

门槛复跑(对应 DEMO-功能测试流程.md A3/A4): pytest -q --ignore-glob=test_sprint*.py · npm run test。

⚠️ 这两个数字不是实测值,是 docs/答辩/DEMO-功能测试流程.md 里写死的文档值(A3 称 896 passed、A4 称 27 passed)。Phase 0 必须重新实测并以此为准——若实测与文档值不符,以实测为准,并把文档值的偏差作为一条发现记录(该验收文档本身已被查出门槛陈旧,见 1.4)。同理,交易四文件单测的 76 passed 是上一轮实跑值,Phase 0 同样需复测确认未退步。


六、腐化资产修复台账(测试基建,单独记账)

按口径 5 修,但与业务缺陷分开记账。修复只碰测试脚本自身,不碰 app/** 与 web/src/**。四个包的报告里,本台账单列一节,不计入业务缺陷数。

# 文件 修法
F-01 03-risk.mjs:23 把 ok(..., true, ...) 恒真断言改为对实测值的真实断言(先记录当前实际返回值,再据此写期望)
F-02 06-chat.mjs:12 /app/analyst/chat → /app/analytics/chat(以 web/src/routes/menus.tsx 为准)
F-03 08-edge.mjs:15,41 更新过期选择器;修完后确认不再在 :42 抛异常
F-04 11-cross-role.mjs:104 更新 C1.6 的过期前提
F-05 09/10/11 的 harness 指向 从仓库外 harness 改为 scripts/e2e/harness.mjs,核对 login/clearAuth/waitRows/issues 原语(见 1.5 分叉事实)
F-06 截图路径参数化 各脚本 shots 固定常量 → 按包名拼路径(防四包互相覆盖)

只记录不修: run_demo_smoke.ps1(已失效)· scripts/eval/evaluate_agent.py:49,59 · docs/演示文档/功能演示版验收标准.md 的陈旧门槛 · 19 个 test_sprint*.py 的 65 条失败 · 无 CI / 无覆盖率 / 无统一入口。 → 理由:这些要么是文档/残留、要么修复属工程基建立项,超出本轮"补测"范围。在横切包 XC 的"剩余风险"节列明。


七、数据副作用:三层防线

问题本质 —— 同一批表被多条用例串行读写,前一条污染后一条的前置(顾问线 3 条假 FAIL 的成因)。

第 1 层(必做,零成本):断言一律用「前后差值」,禁止绝对计数。 before 在同一条用例内紧邻采集;批次类断言按业务键定位(WHERE customer_id=? AND product_id=?)而非行号/主键;幂等键一律 E2E-<线>-<runstamp>-<nn> 每轮唯一(固定键重跑会命中上一轮的单,语义退化成"非首次受理")。 → 这层保证污染不会造成假 PASS/假 FAIL;快照只是回滚手段,不是正确性前提。

第 2 层(本轮默认):跑前 mysqldump 快照,跑后还原。 范围仅 core_trade / core_holding / core_share_lot / core_convert_request / core_convert_lot_detail(jinrong_core),--single-transaction --add-drop-table,跑后 mysql jinrong_core < snap.sql 恢复。 刻意不还原: core_product / core_product_nav / core_customer_advisor / 交易日历(只读,还原反破坏基线);jinrong_agent 的 risk_alert / risk_suitability_log / audit_log —— 追加型审计日志,按语义永不回滚,回滚反而破坏审计链。 风险: 还原会一并回滚本轮之前的手工演示态。执行前先查 core_trade 是否含非本测试前缀的近期行;若有且重要,先单独导出。

第 3 层(兜底,本轮不用):scripts/core/reset.ps1。 会 DROP 重建 jinrong_core(丢全部手工演示态)、不动 jinrong_agent → agent 库残留指向已消失 Core 数据的引用,使"用 agent 库对账"的用例结论不可信。仅当快照不可用时启用,且须在报告中显式声明基线变更。


八、风险与假结论清单

# 风险 处置
R1 _maintain_lots(app/gateway/trade_gateway.py:281-467)整段 try 包住、失败不阻断交易 → 「交易成功但批次没动」的假 PASS。这是本轮最容易出假 PASS 的点 写侧用例同时断言四条链:core_trade 新行 且 core_share_lot 批次变动 且 core_holding 快照变动 且 GET /holdings 返回值变动。四点缺一即 FAIL,不允许"trade_id 非空就算过"
R2 LLM 随机性导致问数线/对话类假 FAIL(AN3、AN4、AD7 等大面积依赖真实 LLM) 硬断言只放不依赖 LLM 语义的部分:HTTP 状态码、字段结构、SQL 只读性、预警零增量、鉴权。凡"回复必须提到 X 关键词"一律降级为 soft check(记录实际,不计入 FAIL 分母)
R3 AD8 是「预期 FAIL」的取证组,容易被误读成回归退化 在 README.md 结论行与测试日志显式标注「AD8 为接缝取证,FAIL 是预期结果」;不计入通过率,单独列节
R4 顺序敏感用例:上一轮 CT3-05 踩过 15 分钟墙钟窗口坑(audit_log.created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE) + jinrong_agent 永不还原 → 距上轮 <15 分钟复跑假 FAIL) 同类窗口查询一律距上轮 ≥15 分钟;或改按本轮 runstamp 过滤(更优)。执行顺序里显式留窗口
R5 四个包共用一个 Vite 5173,clearAuth 切角色并发会互顶 localStorage Part B 严格串行;每个包跑完先 clearAuth 再交接
R6 截图互相覆盖(上一轮已丢 v1.0 截图,是记录在案的证据链缺口) 见 F-06:执行前先参数化截图路径,否则四包截图互毁
R7 Vite 绑 [::1] 而脚本用 localhost 若 ECONNREFUSED,回退 BASE=http://127.0.0.1:5173(须确认 Vite 也听 IPv4)。不做自动重试(会掩盖真实配置问题),报告中记录实际 BASE
R8 收复 09/10/11 时 harness 语义分叉(仓库内 9301 B vs 仓库外 6049 B) 逐支核对原语后再改指向;先跑通一支再收下一支,不要批量 cp 后一起调
R9 问数线数据域(self/assigned/full/risk/aggregate)与 sql_guard 的边界口径未知 AN2 的跨域探针期望值以实测冻结核对,不预设
R10 未知客户/未知资源 404 vs 200+blocked 的边界二义 一律用两个探针(格式合法未知 ID / 格式非法),期望值以实测冻结

九、边界(本轮不做)

  • 不改任何业务代码(app/**、web/src/**)—— 发现的缺陷只进报告。AD8 取证的 3 处工具绑定错误只报告,不在本轮修。
  • 不改 tests/conftest.py · scripts/core/reset.ps1 · scripts/dev/restart-dev.ps1
  • 不动既有 advisor_e2e_smoke.py / risk_e2e_smoke.py 的既有断言(只新增分组)——它们是回归基线,改了会让 FAIL→PASS 对照失效
  • 不新增浏览器框架依赖(不动 web/package.json)· 不新增 Python 依赖(httpx/pymysql 已有)
  • 第 1.6 节基建缺口与第六节"只记录不修"项只记录

十、执行顺序

原则:先证明「被测进程是当前代码」→ 再证明「事实基线」→ 再只读对账 → 再写侧 → 最后浏览器 → 最后回归。

Phase 0 准备(串行)

  1. 确认 8000 / 5173 / Redis 6380 均在跑;记录 uvicorn PID 与启动时间(--reload 场景下确认已加载当前代码)
  2. mysqldump 5 表快照 → /tmp/e2e-gap/snapshot.sql
  3. 先做 F-06(截图路径参数化)与 F-01~F-05(资产修复) —— 必须早于任何 Part B,否则四包截图互毁、且三支脚本跑不出可信结论
  4. 实测并按结果锁定四项门槛基线(写在报告里当作"不得退步"的基准):npm run build(类型闸门,秒级)· 四文件交易单测(上一轮实测 76 passed)· pytest -q --ignore-glob=test_sprint*.py(文档称 896)· npm run test(文档称 27)。 后两项的数字来自文档而非实测,此处必须重测;若与文档值不符,以实测为准,并把偏差记为一条发现(该验收文档已被查出门槛陈旧,见 1.4)
  5. 复核 1.2 的前端路由分档(12+13+3=28≠30,须以 web/src/routes/menus.tsx 实际路由表重新分档)
  6. 记录 core_trade 既有非测试前缀行(供还原前取舍判断)

Phase 1 只读组(三条线并行,无写所以可并行) 各线 preflight + 鉴权矩阵 + 契约冻结 + 基线加载;问数线 AN1/AN2、理财师 AD8(纯读工具调用)、横切 XC2/XC3 只读部分。

Phase 2 写侧组(串行) 理财师 AD7/AD10 → 横切 XC1/XC4 → 风控 RK7。问数线无写侧(红线:仅 SELECT、不处置预警)——这本身就是 AN3 的断言内容。

Phase 3 写侧后对账 各线读侧对账;采 core_trade 等五表首尾计数差供还原核对。

Phase 4 Part B 浏览器(四包严格串行) 问数线(含 04-analyst-market-advisor.mjs 收复 + 表单提交深挖)→ 理财师线(09 收复)→ 风控线(10 收复 + RK8 恒真修复对照)→ 横切(11 收复、07-responsive-a11y.mjs、/register)。 LLM 依赖最重的对话类用例放各自包的最后,失败不阻塞整轮结论。

Phase 5 Part C 回归 09 → 10 → 11(改指向仓库内 harness 后)→ 重跑四文件交易单测 + npm run build,确认不退步。

Phase 6 还原与报告 mysql jinrong_core < snapshot.sql → 用 Phase 3 的计数对照校验 5 表回到首值 → 写四个包 → 更新 docs/memory/MEMORY.md 与 docs/memory/TODO.md。


十一、验证方式

# 0) 前置:uvicorn 8000 + Vite 5173 + Redis 6380 均在跑
curl -s http://127.0.0.1:8000/api/ready            # 期望 ok=true, redis=true
curl -s http://127.0.0.1:8000/openapi.json | python -c "import sys,json;print(len(json.load(sys.stdin)['paths']))"   # 基线 66

# 1) 快照
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/e2e-gap/snapshot.sql

# 2) 门槛基线(先在 Phase 0 实测锁定,之后不得退步)
#    文档值 896 / 27 仅供参考,实测不符时以实测为准并记录偏差
cd web && npm run build && npm run test && cd ..
python -m pytest -q --ignore-glob=test_sprint*.py            # 文档称 896 passed,须实测
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,须复测

# 3) Part A 真实 HTTP E2E(退出码 0 全过 / 1 有 FAIL / 2 前置失败)
python scripts/dev/analyst_e2e_smoke.py  --report /tmp/e2e-gap/an.md
python scripts/dev/advisor_e2e_smoke.py  --only AD7 --only AD8 --only AD9 --only AD10 --report /tmp/e2e-gap/ad.md
python scripts/dev/risk_e2e_smoke.py     --only RK7 --report /tmp/e2e-gap/rk.md
python scripts/dev/crosscut_e2e_smoke.py --report /tmp/e2e-gap/xc.md

# 4) Part B 真浏览器(须 Vite 5173;严格串行,勿并发)
cd scripts/e2e && node 04-analyst-market-advisor.mjs
node 09-advisor-pages.mjs && node 10-risk-pages.mjs && node 11-cross-role.mjs && node 07-responsive-a11y.mjs

# 5) 还原并核对(用 Phase 3 的计数对照)
mysql -h127.0.0.1 -uroot -p jinrong_core < /tmp/e2e-gap/snapshot.sql

完成判据:

  1. 三条线各自 preflight 通过(openapi 必需路由齐全,路径总数 66)
  2. pytest 896 passed 不退步 · 交易四文件 76 passed 不退步 · npm run build 与 npm run test 退出码 0
  3. 每条 FAIL 都附可复现命令 + 实测输出(verification-before-completion 铁律:没跑过的验证不能写成通过)
  4. 每条业务缺陷都给了根因(systematic-debugging:不得只报症状就提修法)
  5. AD8 的预期 FAIL 与业务缺陷分列,不混入通过率分母;INFO/SOFT 同样不进入分母
  6. 5 张写表计数还原到首值
  7. 测试基建修复台账(F-01~F-06)与业务缺陷清单分开记账

十二、产出物清单

新建(脚本)

  • scripts/dev/analyst_e2e_smoke.py(问数线,AN1–AN12)
  • scripts/dev/crosscut_e2e_smoke.py(横切,XC1–XC5)
  • scripts/e2e/09-advisor-pages.mjs · 10-risk-pages.mjs · 11-cross-role.mjs · 04-analyst-market-advisor.mjs · 07-responsive-a11y.mjs · 03-risk.mjs · 06-chat.mjs · 08-edge.mjs(从仓库外收复,含 F-01~F-06 修复)

扩充(脚本,只新增分组)

  • scripts/dev/advisor_e2e_smoke.py(+AD7–AD10)
  • scripts/dev/risk_e2e_smoke.py(+RK7–RK8)

报告包(四个)

  • docs/memory/tests/2026-09-13-analyst-e2e/ · -advisor-e2e/ · -risk-e2e/ · -crosscut-e2e/ 各含 README.md + TEST-LOG-*.md + _raw/(含 shots/、脚本原生输出、results-*.json)

文档更新

  • docs/memory/MEMORY.md(§0 加第五轮行 + 新发现)· docs/memory/TODO.md
  • docs/superpowers/plans/2026-09-13-e2e-coverage-gap.md(本计划的归档副本)