- 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.
40 KiB
端到端覆盖缺口补测轮 · 三条线并跑(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.pyC3→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 |
| 取消——原样保留作 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 不退步"已不成立。(本轮最高价值发现) 证据链完整:
git show 5c18164 -- app/service/convert/convert_service.py显示该 commit 把if suit.blocked:→if suit.blocked or simulate_self_service_blocked(suit):(就是上一轮的 P1 修复)。- 该 commit 同步更新了
tests/test_convert_accept.py(43 行),但漏改了tests/test_convert_confirm.py。 - 后者
tests/test_convert_confirm.py:42仍写着CUST = "CUST-CC" # C3 客户 → 转入 R4 = 放行——固化的是"R4 转入=放行(需揭示)"这条旧语义。 - 新语义下该路径走入阻塞分支;而
convert_service.py:522-535的阻塞返回体不含forced_full_transfer键 →tests/test_convert_confirm.py:1013的accepted["forced_full_transfer"]KeyError→ 28 条失败。 - 另有
tests/test_convert_accept.py::test_accept_uk_idem_race_falls_back_to_idempotent1 条失败(疑与该 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 准备(串行)
- 确认 8000 / 5173 / Redis 6380 均在跑;记录 uvicorn PID 与启动时间(
--reload场景下确认已加载当前代码) mysqldump5 表快照 →/tmp/e2e-gap/snapshot.sql- 先做 F-06(截图路径参数化)与 F-01~F-05(资产修复) —— 必须早于任何 Part B,否则四包截图互毁、且三支脚本跑不出可信结论
- 实测并按结果锁定四项门槛基线(写在报告里当作"不得退步"的基准):
npm run build(类型闸门,秒级)· 四文件交易单测(上一轮实测 76 passed)·pytest -q --ignore-glob=test_sprint*.py(文档称 896)·npm run test(文档称 27)。 后两项的数字来自文档而非实测,此处必须重测;若与文档值不符,以实测为准,并把偏差记为一条发现(该验收文档已被查出门槛陈旧,见 1.4) - 复核 1.2 的前端路由分档(
12+13+3=28≠30,须以web/src/routes/menus.tsx实际路由表重新分档) - 记录
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
完成判据:
- 三条线各自
preflight通过(openapi 必需路由齐全,路径总数 66) pytest896 passed 不退步 · 交易四文件 76 passed 不退步 ·npm run build与npm run test退出码 0- 每条 FAIL 都附可复现命令 + 实测输出(verification-before-completion 铁律:没跑过的验证不能写成通过)
- 每条业务缺陷都给了根因(systematic-debugging:不得只报症状就提修法)
AD8的预期 FAIL 与业务缺陷分列,不混入通过率分母;INFO/SOFT同样不进入分母- 5 张写表计数还原到首值
- 测试基建修复台账(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.mddocs/superpowers/plans/2026-09-13-e2e-coverage-gap.md(本计划的归档副本)