Files
zhanghongyu_0626 01db9af32b fix(memory): Resolve multi-turn dialogue defects and address existing test bugs
- Fixed three key defects in multi-turn dialogue handling: improved RAG to utilize merged messages, enhanced intent classification with recent memory, and ensured proper merging of chat and consultation memories.
- Closed five existing test bugs unrelated to the current round, including parameter adjustments and assertion corrections in various test files.
- Finalized adjustments in the F-β selection process to align race condition handling in test cases, ensuring all relevant tests pass successfully.

This update enhances the coherence of multi-turn dialogues and improves the reliability of the testing framework.
2026-09-14 00:59:25 +08:00
..

TEST-XC-001 · 横切包(跨角色 / 前端全局 / 测试基建台账 / 基建缺口)

当前结论(2026-09-13):

部分 内容 结果
Part A 孤立路由补测(GX1–GX5) 33 PASS / 0 FAIL / 1 SKIP / 19 INFO(退出码 0)
Part B 浏览器跨角色与全局(11-cross-role · 07-responsive-a11y · 06-chat · 08-edge) 59 PASS / 0 FAIL / 3 INFO(4 支全部退出码 0)
Part C 回归门槛复跑 npm run build 退出码 0 · npm run test 27 passed (8 files) · 四文件交易单测 49 passed / 28 failed(见 F-β) · pytest 1236 passed / 102 failed / 9 skipped / 21 errors

本包的结论边界: 跨角色用例跨三条线、腐化资产是测试基建而非任何一条线的业务缺陷 —— 塞进任一业务线包都会污染那个包的结论边界,故单独成包。

读数纪律(沿用 TEST-CT-001 / TEST-AN-003): ① INFO 不是 PASS,不得计入通过率分母;② 测试基建台账(F-α…F-ζ / F-01…F-06)与业务缺陷分开记账,不计入业务缺陷数;③ 本包只有一条 FAIL 类结论(F-β 的 28 条单测失败),且已定位到根因:测试前提固化旧语义(非接口缺陷),非本轮引入 —— 见 §8.2。


一、产物

产物 路径 位置
企业级测试日志 TEST-LOG-2026-09-13-XC-001.md 本包
孤立路由补测脚本(GX1–GX5) scripts/dev/gap_routes_e2e_smoke.py 仓库内(本轮新建)
浏览器脚本(本轮自仓库外收复) scripts/e2e/11-cross-role.mjs · 07-responsive-a11y.mjs · 06-chat.mjs · 08-edge.mjs 仓库内

二、结果明细 _raw/

文件 内容 结果
xc-gap.md GX1–GX5 逐条矩阵 33 PASS / 0 FAIL / 1 SKIP / 19 INFO
11-cross-role.stdout.txt 跨角色浏览器 9 PASS / 0 FAIL
07-responsive-a11y.stdout.txt 响应式 + 无障碍 26 PASS / 0 FAIL
06-chat.stdout.txt 四线对话/问数入口 7 PASS / 0 FAIL / 2 INFO
08-edge.stdout.txt 边界(未登录/未知路由/游客试聊/处置/刷新) 17 PASS / 0 FAIL / 1 INFO
results-11-cross-role.json 跨角色原生 JSON —
_f-eps-r2.log F-ε 污染测量与还原(红字=污染,见 §4) —
snapshot-script_template-r2.sql F-ε 还原前的可逆快照(26064 B) —
snapshot-postrun.sql Phase 6 还原前的 jinrong_core 5 表快照(50762 B) —
restore_script_template.py F-ε 还原脚本(只还原,不动业务) —
_partc-pytest-A.txt · -B.txt 连跑两次的 FAIL/ERROR 清单(用于证明套件非幂等,见 F-η) A=129 行 / B=127 行
_partc-pytest-nonsprint.txt 修正 --ignore-glob 后的非 sprint 套件结果(见 F-ζ) 45 failed / 1199 passed / 9 skipped / 21 errors
shots/ 21 张截图(含 vp320-* 五角色响应式、游客试聊、处置弹窗) —

三、Part A · GX1–GX5(孤立路由)

这些路由不属于任何单条业务线,故收进本包:★threshold-check(此前连单测都只有字符串断言)· staff/me · products/{id} · chat 会话侧栏。

组 覆盖对象 PASS FAIL SKIP INFO
GX1 POST /api/customers/{id}/threshold-check ★(三角色 + 越权 + 未知客户) 13 0 0 5
GX2 GET /api/staff/me(含 customer token 与无 token 负例) 5 0 0 1
GX2b staff 型 token 载客户号的实测行为(见下「token 型陷阱」) 2 0 0 1
GX3 GET /api/products/{product_id}(含未知产品 404 与无 token) 5 0 0 1
GX4 chat 会话侧栏(close-all / sessions / {id}/close) 8 0 1 3
GX5 文档类发现(编号歧义 + 陈旧门槛) 0 0 0 8
合计 33 0 1 19

GX 的关键取证

用例 断言 实测
GX1-01 ★零覆盖路由 threshold-check 三角色各自可用 risk_officer / customer 本人 / 归属 advisor 均 HTTP 200,返回体含 alert + pushed 两键
GX1-01b 未带 push 参数 ⇒ pushed=false pushed=False(三角色一致)
GX1-03 非本人 customer 查 CUST-1001 → 拒 HTTP 403 AUTH_403_NOT_OWNER
GX1-04 非归属 advisor 查 CUST-1004 → 拒 HTTP 403 AUTH_403_NOT_ASSIGNED
GX1-05 未知客户 → 200 且 alert=null(不 404) HTTP 200 alert=None
GX2-01 staff/me 身份一致性 staff_id='STAFF-10086';返回字段 ['display_name','roles','staff_id','staff_type']
GX2-01c roles 是 list 而非 JSON 字符串 list
GX3-03 未知产品 → 404 HTTP 404 NOT_FOUND
GX4-03a close-all 返回体为裸 dict {'agent_type': 'analyst', 'closed_count': 0}
GX4-04 close-all 幂等(紧接着再调) HTTP 200,closed_count=0
GX4-06 关闭不存在的会话 → 404 HTTP 404 NOT_FOUND

零副作用设计: GX4 的写操作刻意用 STAFF-20003("问数第三账号",非演示账号)。该账号实测会话数 n=0,故 close-all 返回 closed_count=0 —— 可证明地零副作用,不会动到演示叙事所需的会话。

一处 SKIP(诚实登记): GX4-08「他人会话越权 → 403」因无可用对照会话(该账号无会话)而 SKIP;同理 GX4-07「已关闭会话再关闭 → 409」的状态机分支未被覆盖,已记为未覆盖路径。

★ 本轮最有价值的跨切发现:token 型陷阱(GX2b)

login() 默认 token_type="staff"。用客户账号以默认值登录,签发的 JWT 是 token_type='staff', roles=['customer'], customer_id=None。而 assert_customer_access 的 self 域靠 customer_id 判定 ⇒ 客户查自己的数据也会 403 AUTH_403_NOT_OWNER。

这不是产品缺陷,是调用方必须显式传 token_type="customer"。 但它的杀伤力在于会伪造 FAIL —— 本轮我在三支脚本里都因它产出过假 FAIL(GX / advisor gap / risk gap),直到逐条 root-cause 才发现是我的脚本写错。

实测另一面(GX2b-01):staff 型 token 载客户号打 /api/staff/me → 404 NOT_FOUND(不是 403)。根因:require_staff_token(app/utils/deps.py:353-356)只比 auth.token_type == "staff",不比 role ⇒ staff 型即放行,随后 staff_service.get_staff_me('CUST-9527') 查无此人 → 404。记为实测事实,不记为缺陷判定。


四、Part B · 浏览器(4 支串行,全绿)

脚本 结果 覆盖要点
11-cross-role.mjs(F-04 修复后) 9 PASS / 0 FAIL / 0 SKIP 跨角色直达被拦:C2.2/C2.3 顾问身份直达风控页 → 打开=false,hash 停在 #/app/advisor/home,后端 HTTP 403
07-responsive-a11y.mjs 26 PASS / 0 FAIL / 0 INFO 键盘 Tab 焦点环可见(outline: solid 2px)· vp320 五角色布局
06-chat.mjs(F-02 + F-14 修复后) 7 PASS / 0 FAIL / 2 INFO 四条线问答入口:风控 SSE · 顾问 · 问数工作台(非"对话页")· 客户同步
08-edge.mjs(F-03 修复后) 17 PASS / 0 FAIL / 1 INFO 未登录 → 回登录页 · 未知路由 → 角色首页且不崩 · 连点登录只发 1 次请求 · 游客试聊(卡片 + 真实回复 + 空输入禁用)· 处置弹窗闭环 · 四角色刷新不崩

Part B 合计:59 PASS / 0 FAIL / 3 INFO,4 支全部退出码 0。(连同其余四包,Part B 全轮 123 PASS / 0 FAIL / 6 INFO,8 支脚本。)

两处未修复的脚本设计缺陷(本轮降级为 INFO 并留证)

  1. 06-chat.mjs 的"对话页"前提对问数线不成立(F-02 的深层面)。 原脚本按 #/app/${role}/chat 拼路径,对 analyst 拼出不存在的 /app/analyst/chat。根因不是拼错,而是:web/src/App.tsx 中 /app/analytics/chat 是 <Navigate to="/app/analytics/query" replace/>,数据分析线根本没有独立对话页。故修法是承认这一点,改打真实入口「问数工作台」。
  2. 客户回复的免责声明断言越界(F-14)。 详见下「F-14」。

五、Part C · 回归门槛复跑(含一条坏基线:F-β,根因已查清)

门槛 文档/上轮声称 本轮实测 判定
npm run build(类型闸门) 退出码 0 退出码 0(built in 2.11s) ✅ 未退步
npm run test 27 passed 27 passed (8 files) ✅ 一致
四文件交易单测 76 passed ❌ 49 passed / 28 failed ⚠️ F-β
pytest -q --ignore-glob=test_sprint*.py 896 passed ❌ 1236 passed / 102 failed / 9 skipped / 21 errors ⚠️ F-α + F-ζ
pytest(修正 glob,非 sprint) — 1199 passed / 45 failed / 9 skipped / 21 errors 新的可信基线

六、测试基建台账(修,但与业务缺陷分开记账)

口径 5:修,但单独记账。修复只碰测试脚本自身,不碰 app/** 与 web/src/**。

6.1 已修(F-01 ~ F-06)

# 文件 问题 修法 验证
F-01 03-risk.mjs:23 ok(..., true, ...) 恒真断言 —— 永远 PASS,不构成证据 改为对实测值的真实断言 风控首页 KPI 为真实正整数(非 0/非占位),实测 heroTotal:37。不写死具体数字(预警数随演示动作变化,写死会假 FAIL) 实跑 14 PASS / 0 FAIL;并由 RK8 反证:原 21 条 PASS 修复后全部仍 PASS,退化集 ∅
F-02 06-chat.mjs:12 构造了不存在的路由 /app/analyst/chat 改为问数工作台 /app/analytics/query(承认该线无对话页,见 §四.1) 实跑 PASS
F-03 08-edge.mjs:15,41 选择器过期,预计在 :42 抛异常 更新过期选择器 实跑 17 PASS / 0 FAIL,未抛异常
F-04 11-cross-role.mjs:104 C1.6 前提已过期 更新前提 实跑 9 PASS / 0 FAIL
F-05 09/10/11 的 harness 指向 仓库外 harness(6049 B)与仓库内(9301 B)已分叉,不是同一份 逐支改指向 scripts/e2e/harness.mjs,核对原语 三支均跑通(见 R8:先跑通一支再收下一支)
F-06 截图路径 _raw/shots/ 是固定常量,四包会互相顶掉(上轮已丢 v1.0 截图) 降级为零代码改动:harness.mjs:39 早已是 process.env.E2E_SHOT_DIR || …,各包给不同 E2E_SHOT_DIR 即可 四包截图各自完整(本包 shots/ 21 张)

6.2 本轮新发现(F-α · F-β · F-γ · F-ε · F-ζ · F-η)

F-α —— 验收文档的门槛数字与实测严重不符

docs/答辩/DEMO-功能测试流程.md A3 称 896 passed;实测 1236 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 不退步"改为"不退步于实测基线"。

F-β —— 上一轮的 P1 修复打断了 28 条单测,且"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 条失败。

本轮复跑实测 49 passed / 28 failed,与 Phase 0 完全一致 ⇒ 证明是既有问题,不是本轮引入。这正好实证了上一轮 MEMORY.md 里「⚠️ 复跑新发现(需拍板)」的第 ② 条:「P1 修复可能收窄了业务能力」。 → 只报告不修。更新 2026-09-13:此项已查清 —— 业务上 R4 的"需揭示 → 自助端阻断"方向正确(L0 矩阵刻意设三态 + 答辩稿「C3×R4 揭示 block」+ 前端 canSelfServe:false),28 条失败是测试前提过期而非接口缺陷。完整依据、建议与反向禁忌见 §8.2。 → ✅ 已修(2026-09-13 修复轮 · 选 A):test_convert_confirm.py 客户档位 C3→C4(C4×R4=allowed),四文件 77 passed / 0 failed;零业务代码改动,未补 forced_full_transfer(遵守反向禁忌)。 → ✅ 收尾(2026-09-14):另 1 条 test_convert_accept.py::test_accept_uk_idem_race_falls_back_to_idempotent(L1 同键异体→409 后,竞态用例「抢占者」份额 100≠50 四元组不匹配)→ 抢占者份额改 50 对齐;交易五文件(含 convert_accept)99 passed / 0 failed。

**F-γ —— core_share_lot 的基线读数与快照不符(本轮更正口径)

上一轮 README 载明还原目标 95/59/73/19/0;本轮 Phase 0 跑前实测 95/59/74/19/0(多 1 行)。 但 Phase 6 还原时发现:Phase 0 的 snapshot.sql 自身只含 73 行(经导入独立 scratch 库逐行计数确认)。

⇒ Phase 0 的"74"读数与 1 分钟后的快照不自洽(疑为读数与 dump 之间有行变动)。 处置:还原判据改为「与快照内容逐表 CHECKSUM 一致」,而非「等于某个读数值」 —— 快照才是权威可复现的产物。见 §七。

F-ε —— pytest 直接污染真实的 jinrong_agent 与 jinrong_core(影响最广)

根因: scripts/seed/import_script_templates.py:14 在模块级绑定了 AgentSessionLocal,而 tests/test_sprint2_template_import.py 的 seed 调用未请求 _advisor_agent_sqlite_db fixture ⇒ 测试写进了真库。

实测污染(本段直接测量):

指标 pytest 前 4 次 pytest 后
script_template 总行数 47 65
created_by='test:*' 19 36
seed:template_test_data 的 is_approved 20 行全为 1 20 行全被翻成 0
页面可见模板数(approved AND active) 20 6

用户可见后果: import_templates_from_markdown 按数据集里的 is_approved=False 覆写,而 .ps1 的"审核启用"收尾步骤未被 pytest 执行 ⇒ 演示用的 20 条话术模板集体从页面消失。

且不止 jinrong_agent: jinrong_core 5 表在 Phase 3 读数 102/62/81/23/0,到 Phase 6 还原前已变成 103/62/86/26/0 ⇒ 4 次 pytest 又往核心业务库写了 +1 trade / +5 share_lot / +3 convert_request。

已还原(可复现):

  • 还原前先取可逆快照 snapshot-script_template-r2.sql(26064 B,较 20:56 那次的 15980 B —— 多出的部分就是污染)。
  • restore_script_template.py 只做两件还原动作:① created_by LIKE 'test:%' → is_active=0(置隐而非 DELETE:既完全恢复页面可观测状态,又把污染证据留在库里可查);② is_approved=1 复原那 20 条演示模板。
  • 实测结果:页面可见行数 = 20(回到污染前),退出无异常。

纪律说明: 探针写进真库而非 sqlite 假库,是本项目"19 个 test_sprint*.py 伪造全部 Core 数据源"的反面同源问题 —— 有的是"假数据源导致测不出接缝",有的是"真数据源被测试写坏"。两者都值得在修复轮立项。

✅ 已修(2026-09-13 修复轮):根因实为测试文件模块级 from app.advisor_db import AgentSessionLocal 在收集期绑定真库 sessionmaker、绕过 fixture 的 monkeypatch(不单是 import_script_templates.py:14)。修法:① 10 个 test_sprint*.py 改 from app import advisor_db + 调用点 advisor_db.AgentSessionLocal/advisor_db.agent_engine;② import_script_templates.py 同改;③ 唯一写真库的 subprocess 用例(seed 脚本独立跑)改 @pytest.mark.skip(另两处 subprocess sync_template_vectors --dry-run / evaluate_compliance 经查只读)。验证:跑后 script_template 仍 65 / seed 20 / test:* 36,零污染。同轮另修:6 个 test_sprint2_*.py 的陈旧登录契约(/api/v1/auth/login + {username,password} → tests/advisor_test_utils.login_token)。副作用(如实):转 sqlite 后暴露 F-α 假 DDL 缺列(agent_session 无 metadata 等),sprint 组 45 条失败为陈旧 DDL/期望,非本轮引入。

F-ζ —— --ignore-glob=test_sprint*.py 是空操作**(本轮新发现)**

docs/答辩/DEMO-功能测试流程.md A3 的门槛命令写着 pytest -q --ignore-glob=test_sprint*.py。实测该 flag 一个文件都没过滤掉:

命令 收集用例数 其中来自 test_sprint*
--ignore-glob=test_sprint*.py 1368 94
(不带该 flag) 1368 94
--ignore-glob='*/test_sprint*.py'(修正后) 1274 0

根因: pytest 的 --ignore-glob 匹配的是相对 rootdir 的完整路径,test_sprint*.py 里没有 */ 去覆盖 tests/ 这一层,故无一命中。 后果: 文档口径里"排除 sprint 测试后的 896 passed"从来就不是排除后的数字 —— 这个门槛自建立起就没有按设计流过。 处置: 记录并给出修正写法。本轮同时报告修正后的真实基线(1199 passed / 45 failed / 21 errors / 9 skipped)。

F-η —— 测试套件非幂等**:连跑两次结果不同**

同一个库、相隔数分钟、同一条命令,连跑两次的 FAIL/ERROR 集合不一致:

轮次 FAILED+ERROR 行数 差异
A 129 独有 2 条:test_sprint2_market_alert_feedback.py::test_feedback_api_requires_auth_and_returns_trace · test_sprint2_market_alert_generation.py::test_generation_contains_four_sections_and_persists_compliance_result
B 127 上述 2 条通过

根因(与 F-ε 同源): 测试写真实库且不回滚 ⇒ 污染逐轮累积 ⇒ 后续跑批读到的状态已变。Phase 0 的 1232 passed 与还原后的 1236 passed(FAIL/ERROR/SKIP 完全相同,仅 passed +4)亦属此类:两轮之间唯一的已知库侧变化是 20:56 对 script_template 的还原。

⚠️ 结论级影响: 单次 pytest 的 passed 数不是可靠的"不退步"基线 —— 它随库状态漂移。凡引用该数字,必须同时声明跑批序号与库状态。

6.3 只记录不修(超出本轮"补测"范围)

# 项 事实
1 scripts/demo/run_demo_smoke.ps1 已失效:仍打已不存在的 /api/v1/*(19 处引用)⇒ 唯一 demo 冒烟脚本会 404
2 scripts/eval/evaluate_agent.py:49,59 同打 /api/v1/*(2 处)⇒ 已失效
3 docs/演示文档/功能演示版验收标准.md 门槛陈旧:/api/v1/ping 在当前 openapi 中不存在(实测);alembic heads 写的是旧版本号;"连续执行 run_demo_smoke.ps1 通过"这条已无法成立
4 19 个 test_sprint*.py 共 65 条失败;且伪造全部 Core 数据源 ⇒ 结构上不可能发现接缝不匹配(实证见理财师包 ADV-3)
5 无 CI · 无覆盖率工具 · 无 Makefile / 统一测试入口 工程基建立项

6.4 ⚠️ 文档缺陷:D2/D3 一号两义(答辩稿内,须拍板)

docs/答辩/DEMO-功能测试流程.md 中同一符号指两件事(实测共 4 行,同时含 D2 与 D3 的行 = L89):

行 原文 语义
L64 ` D2
L65 ` D3
L79 ` E5
L89 ` F2

处置(已在本轮四个包内执行): 本报告体系一律不使用裸 D2/D3,改写为「问数线 D2」或「ADV 缺陷-2」这类带前缀写法。该文档缺陷本身只报告不修(修答辩稿属文档轮次)。


七、Phase 6 · 数据还原与校验(以快照为准,逐表 CHECKSUM)

还原前先取可逆快照 snapshot-postrun.sql(50762 B),再 mysql jinrong_core < snapshot.sql。

表 还原后行数 快照行数 还原后 CHECKSUM 快照 CHECKSUM 一致
core_trade 95 95 894871061 894871061 ✅
core_holding 59 59 844767797 844767797 ✅
core_share_lot 73 73 4247539445 4247539445 ✅
core_convert_request 19 19 1539189241 1539189241 ✅
core_convert_lot_detail 0 0 0 0 ✅

校验方法: 将 snapshot.sql 导入独立的 jr_verify scratch 库,与线上库逐表比对 COUNT(*) 与 CHECKSUM TABLE(不只比行数 —— 行数相同但内容不同是可能的)。校验后已 DROP DATABASE jr_verify。

为什么不用"等于首值 95/59/74/19/0"作判据: 见 F-γ —— Phase 0 的 74 读数与该次快照(73 行)不自洽。快照才是权威可复现的产物,故判据改为「与快照逐表 CHECKSUM 一致」。

刻意不还原(按语义): jinrong_agent 的 risk_alert / audit_log / analytics_query_log / KYC 会话 —— 追加型审计日志,回滚反而破坏审计链。本轮的已登记残留:

  • risk_alert:pending_review 41 → 35(RK7-06 每跑一次 −1,本轮共 4 次含处置跑批 + 08-edge 的浏览器处置用例)。
  • analytics_query_log:随问数跑批单调增长(追加留痕,预期行为)。
  • script_template:36 行 test:* 置 is_active=0 保留(证据留库,页面不可见)。

还原后健康检查: GET /api/ready → ok=true degraded=false,五项 checks(redis / chat_close_all / chat_sessions / analyst_chat / products_nav_history)全 true。


八、遗留待拍板 → 全部落定 ✅(3/3 已拍板,2026-09-13)

更新 2026-09-13(用户拍板后): 原 3 项全部落定 —— F-14 关闭 · RK 发现-1 关闭 · F-β 选 A。本节由「待拍板」改为结论存档。

8.0 ✅ 三项拍板结果一览

# 事项 拍板结论
F-14 收益回复是否强制带规范风险提示 维持现状(接受 LLM 自撰措辞,不扩名单)
RK 发现-1 未知资源「找不到」语义是否统一 404 关闭(不改,保持无 404)
F-β R4「放行+揭示」还是「直接阻断」 选 A:维持阻断语义,改测试(零业务代码改动)

8.1 ✅ 已拍板关闭(用户 2026-09-13 定)

# 事项 拍板结论 影响
F-14 返回收益数字的回复是否应当强制带规范风险提示 维持现状(用户原话「目前够用就行」)—— 接受 LLM 自撰措辞,不扩 compliance_guard.py:109 的 should_add_disclaimer 名单 实质性提示在、规范文案不在。已知代价:措辞与是否出现由 LLM 随机决定。06-chat.mjs 中该条已按 F-14 降级为 2 条 INFO(不计入 PASS 分母)。不再作为待办项
RK 发现-1 未知资源的「找不到」语义是否统一为 404 关闭(用户原话「这个不用管」)—— 不改。现状 officer 200 / advisor 403 / customer 403,无一 404 不泄漏客户存在性(安全上更优),404 的"信息更准"价值不足以推动变更。仅作实测事实保留在本包与风控包,不再作为待办项

8.2 ✅ F-β —— 已拍板:选 A(用户 2026-09-13 定)

拍板结论:维持现状语义不动,下一轮只改测试(客户档位 C3→C4 或产品 R4→R3),零业务代码改动。 规则固化(答辩口径同此):C3×R4 / C4×R5 = 匹配但需揭示 ⇒ 自助渠道阻断并引导线下,这是设计意图,不是缺陷。 禁止项:不得给阻断响应体补 forced_full_transfer 让测试变绿(=让"阻断"伪装成"受理成功")。选项 B(做揭示书 Modal)已否决,若日后要做属独立需求轮。 ✅ 已执行(2026-09-13 修复轮):test_convert_confirm.py 客户档位 C3→C4(C4×R4=allowed),四文件 77 passed / 0 failed;CUST_LOW(C1) 阻断用例保留作 T+1 复核对照;阻断响应体仍不含 forced_full_transfer。 ✅ 收尾(2026-09-14):test_convert_accept.py::test_accept_uk_idem_race_falls_back_to_idempotent 抢占者份额 100→50 对齐 L1 四元组 → 交易五文件 99 passed / 0 failed。

以下为支撑该结论的原始取证(存档备答辩追问):

原始疑问:上一轮 P1 修复改为 blocked or simulate_self_service_blocked(suit) 后,test_convert_confirm.py 固化的旧语义失效,28 条单测失败 —— 修测试还是修实现?

查清结论:现状"阻断"的语义方向是对的,缺陷在测试侧。 三条依据:

  1. L0 权威矩阵刻意设三态(jinrong_core.core_suitability_rule 实测 25 行,全 rule_ref=JR-AST-012):C3×R4 与 C4×R5 = allowed_with_disclosure,既非 allowed 亦非 forbidden。服务层对它返回 matched=True / blocked=False / mismatch_type="none" / block_response_code="SUIT_NEED_DISCLOSURE" ⇒ 它压根不是"不匹配",而是"匹配但须特别风险警示 + 签署揭示书"(《证券期货投资者适当性管理办法》第十九条:主动要求高于承受能力的产品,特别警示并确认后可销售)。
  2. "需揭示 → 自助端阻断"是已定设计,三处实证同指:答辩稿 docs/答辩/答辩知识点清单.md:59 原文即「C3×R4 揭示 block」· 前端 CustomerTradePage.tsx:116「标有『风险不匹配 / 需揭示』的请换产品或到网点办理」· 前端 web/src/utils/tradeEligibility.ts 对该两类情形均给 canSelfServe:false + tone:'warn' + 标签「需揭示/网点办理」。逻辑自洽:揭示需双录/柜面,线上自助无法完成"签署"这个动作,故自助渠道拒绝并转人工。⇒ 上一轮 P1 修复是后端追平前端,方向正确。simulate_self_service_blocked 注释自称"与前端 tradeEligibility 同口径"——该说法经核为真。
  3. 28 条失败根因 = 测试前提过期,非接口缺陷:tests/test_convert_confirm.py:1013 的 assert accepted["forced_full_transfer"] is False,其用例 docstring 明写前提是"受理成功后验证 T+1 只继承、不重判 min_hold";而 forced_full_transfer 是受理成功路径字段("是否被强制全额转换")。CUST-CC(C3) 被阻断后受理未发生、响应体换成阻断体 ⇒ 无该键 ⇒ KeyError ×28。阻断响应体里本就不该有 forced_full_transfer(未受理,谈不上转换方式)。

建议(选项 A,推荐):维持现状语义不动,下一轮改测试 —— 把需要"受理成功"的用例客户从 C3 提到 C4(C4×R4=allowed),或把转入产品从 R4 降到 R3(C3×R3=allowed)。测试内已有该模式(CUST_LOW = "CUST-CCL" # C1 客户 → 转入 R4 = 阻断),作者本就知道要分档客户,只是未跟上语义变更。零业务代码改动,约半小时。

⚠️ 反向禁忌:不得给阻断响应体补 forced_full_transfer 让测试变绿 —— 等于让"阻断"伪装成"受理成功",比测试失败更危险。

选项 B(不推荐,属独立需求轮):若要演示"签揭示书后自助成交",那是把 PRD 二期(放行+提示)提前做,需新建前端揭示书 Modal + 确认留痕 + 后端 requires_disclosure 受理分支。只改后端不建前端 UI 会更糟(变成"毫无提示地放行",比一期口径更不安全)。

8.3 🔴 新增记账(F-β 调查中发现的真缺口,建议只记录不修)

文案引导的「网点办理 / 请联系持证投资顾问」没有对应实现通路。 实测:理财师线(app/api/advisor_*.py)无任何交易类 API(仅 compliance / script_templates / advisors 读取);全仓唯一交易入口是 POST /api/simulate/trade + convert 族,不区分渠道身份。 ⇒ 当前实现里 C3×R4 任何渠道都买不成。与 PRD 一期"不做线下受理"并不矛盾,但文案承诺 > 实现能力,答辩被追问「那网点怎么买」会答不上来。


九、Part B 运行观察(未定根因,如实记录)

# 现象 处置与判定
1 瞬时 502 观测到一次后端 502,随后自愈;未复现,未定位根因。无证据指向产品缺陷,亦不能断言已排除。记录备查
2 Vite 连接抖动 一次 page.waitForURL: Timeout 20000ms exceeded(login(p,'advisor'))。查证:后端 200 · Vite 200 · advisor 登录 200(均经 HTTP 直连复验)⇒ 判定为 dev server 瞬时抖动,重试即成功。属计划 R7 类别(不做自动重试,以免掩盖真实配置问题),记录实际 BASE
3 Vite 绑 [::1] IPv4 127.0.0.1:5173 返回 000,脚本必须用 localhost。已记录

十、复现三连

# 0) 前置:uvicorn 8000 + Vite 5173 + Redis 6380 均在跑
curl -s http://127.0.0.1:8000/api/ready          # 期望 ok=true, redis=true, degraded=false

# 1) Part A · 孤立路由(退出码 0 全过 / 1 有 FAIL / 2 前置失败)
python scripts/dev/gap_routes_e2e_smoke.py --report D:/tmp/e2e-gap/xc-gap.md

# 2) Part B · 四个浏览器脚本(须 Vite;严格串行,勿并发 —— clearAuth 切角色会互顶 localStorage)
cd scripts/e2e
E2E_SHOT_DIR=…/2026-09-13-crosscut-e2e/_raw/shots E2E_OUT_DIR=…/_raw node 11-cross-role.mjs
E2E_SHOT_DIR=… node 07-responsive-a11y.mjs
E2E_SHOT_DIR=… node 06-chat.mjs
E2E_SHOT_DIR=… node 08-edge.mjs

# 3) Part C · 门槛复跑
cd ../../web && npm run build && npm run test && cd ..
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   # 49 passed / 28 failed(F-β)
python -m pytest -q --ignore-glob='*/test_sprint*.py'                                  # 修正后的非 sprint 套件

十一、剩余风险 / 未覆盖(如实声明本轮未做完的部分)

# 项 说明
1 /register(注册链路)仍是零覆盖 计划 XC2 列入,本轮未执行。实测:scripts/e2e/*.mjs 与 scripts/dev/*e2e_smoke.py 无任何脚本引用 register。⇒ 表单校验、重复注册负例均未验
2 /app/home 与 /app 两个路由未访问 同上,XC2 未执行。08-edge 只验了"未知路由重定向到角色首页",不等于访问过 /app/home 本身
3 XC3 的面包屑 / 登出 / 会话侧栏切换未做浏览器验证 GX4 只从 HTTP 侧验了 close-all/sessions/{id}/close;前端按钮点击路径未验
4 测试套件不可复现(F-η) 单次 pytest 结果随库状态漂移。引用其数字必须附跑批序号与库状态
5 F-ε 根因仍在 已修(2026-09-13 修复轮) 10 个 test_sprint*.py 改模块属性访问 + skip 写真库 subprocess 用例;验证零污染(script_template 65/20/36 前后一致)。副作用:转 sqlite 后暴露 F-α 假 DDL 缺列(见 §6.2)
6 28 条交易单测失败未消解 已修 选 A 已执行(客户档位 C3→C4),四文件 77/0。见 §8.2
7 P0 mock 登录无 app_env 门禁 已拍板「本轮不动,只记录」(2026-09-13)。/api/auth/login(app/api/auth.py:16)无条件挂载 ⇒ 生产也能签白名单角色 token。留待部署前修:login() 内 app_env != "development" 即 403/404,或仅 development 条件挂载 router。当前 APP_ENV=development,加门禁不影响 demo
7 GX4-07/GX4-08 两条状态机/越权路径未覆盖 见 §三 SKIP 说明