- 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.
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 并留证)
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/>,数据分析线根本没有独立对话页。故修法是承认这一点,改打真实入口「问数工作台」。- 客户回复的免责声明断言越界(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 不退步"已不成立(本轮最高价值发现)
证据链完整:
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 条失败。
本轮复跑实测 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(另两处 subprocesssync_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_review41 → 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 条单测失败 —— 修测试还是修实现?
查清结论:现状"阻断"的语义方向是对的,缺陷在测试侧。 三条依据:
- 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"⇒ 它压根不是"不匹配",而是"匹配但须特别风险警示 + 签署揭示书"(《证券期货投资者适当性管理办法》第十九条:主动要求高于承受能力的产品,特别警示并确认后可销售)。 - "需揭示 → 自助端阻断"是已定设计,三处实证同指:答辩稿
docs/答辩/答辩知识点清单.md:59原文即「C3×R4 揭示 block」· 前端CustomerTradePage.tsx:116「标有『风险不匹配 / 需揭示』的请换产品或到网点办理」· 前端web/src/utils/tradeEligibility.ts对该两类情形均给canSelfServe:false+tone:'warn'+ 标签「需揭示/网点办理」。逻辑自洽:揭示需双录/柜面,线上自助无法完成"签署"这个动作,故自助渠道拒绝并转人工。⇒ 上一轮 P1 修复是后端追平前端,方向正确。simulate_self_service_blocked注释自称"与前端 tradeEligibility 同口径"——该说法经核为真。 - 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 | 10 个 test_sprint*.py 改模块属性访问 + skip 写真库 subprocess 用例;验证零污染(script_template 65/20/36 前后一致)。副作用:转 sqlite 后暴露 F-α 假 DDL 缺列(见 §6.2) |
|
| 6 | 选 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 说明 |