Files
group_xinghuo_jinrong/docs/memory/tests/2026-09-13-analyst-e2e
zhanghongyu_0626 188bf6a438 feat(analyst_agent): Add support for recent days queries and enhance SQL hint generation
- Introduced `_recent_days_series_hint` to handle queries related to "最近N天" for time series data aggregation.
- Added `_cn_num_to_int` function to convert Chinese numerals to integers for better query parsing.
- Updated `_nl_sql_hints` to incorporate the new hint generation logic, ensuring accurate SQL output for recent days queries.
- Enhanced documentation to reflect these changes and improve clarity on the new functionalities.
2026-09-13 22:36:19 +08:00
..

TEST-AN-002 · 问数线(数据分析 Agent)端到端包

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

  • Part A(真实 HTTP):137 PASS / 6 FAIL / 2 SKIP / 21 INFO,退出码 1。 6 条 FAIL 全部指向 3 个真实缺陷(缺陷 A / A2 / B),无一条是本脚本自身的 bug(已逐条复核,见下「缺陷」节)。按本轮拍板口径只报告不修,修复另开轮次。
  • Part B(真浏览器):04-analyst-market-advisor.mjs 12 PASS / 0 FAIL / 1 INFO,退出码 0。 另由横切包的 06-chat.mjs 覆盖本线真实问答入口(问数工作台)—— 见下「Part B」节。 ⚠️ AnalystAssetsPage 的「提交沉淀」表单仍未提交过(只验了渲染),该项仍是缺口。

读数纪律(沿用 TEST-CT-001): ① INFO 不是 PASS,不得计入通过率分母;② 6 条 FAIL 是 3 个缺陷的 6 个表现,不得读成"6 个缺陷";③ Part B 见下「Part B」节(已执行并归档)。

⚠️ 与既有覆盖的关系(如实声明,勿夸大本包的新增性): 本线此前并非零覆盖。docs/memory/tests/2026-09-11-analyst-domain-rbac(TEST-LOG-2026-09-11-AN-001)与 2026-09-12-analyst-template-phrases(AN-002)已用 fastapi TestClient 进程内方式覆盖了 /chat 的域 RBAC 与模板短语(7 角色、真 DeepSeek、真 MySQL)。 本包新增的是「真实 HTTP 出进程 E2E」:TestClient 不经过 uvicorn、不经过真实 socket、不经过 register_error_handlers 的实际响应路径。经逐文件核对,上述两轮日志中只出现 /chat 与 /template-prompts 两个路由,其余 10 个 handler 无任何脚本级覆盖。 本包编号接续为 AN-002 之后的 TEST-AN-003(见日志)。


产物

产物 路径 位置
企业级测试日志 TEST-LOG-2026-09-13-AN-003.md 本包
问数线真实 HTTP E2E scripts/dev/analyst_e2e_smoke.py 仓库内(本轮新建,约 1.5k 行)
缺陷 A 直连库取证 _raw/defect-A-direct-sql-proof.txt 本包
权威跑批矩阵(3 次一致复跑中的第 3 次) _raw/an-canonical.md + an-canonical.stdout.txt 本包

脚本刻意不 import 服务端代码(ROLE_DATA_DOMAIN / 禁用词表 / 期望值均独立副本)——避免"用被测代码证明被测代码正确"。唯二例外是缺陷取证脚本,它故意调用 _is_timeout_error 以求证其行为。

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

文件 内容 结果
an-canonical.md 权威矩阵(逐条:期望 / 实测 / 备注) 137 PASS / 6 FAIL / 2 SKIP / 21 INFO
an-canonical.stdout.txt 同轮的终端原始输出(含逐条 [ ok ]/[FAIL]/[skip]/[info] 与末尾 FAIL 清单) 退出码 1
defect-A-direct-sql-proof.txt 缺陷 A 的直连库取证(列清单 + 1054 原始异常 + 分级结果 + 影响面 + 三段可复现命令) —

复跑一致性: 同一脚本连续三轮(an-r2 / an-r3 / an-canonical)产出的 FAIL 集合完全一致(同一组 6 条)。全库计数值([170,33] → [193,41] → [216,49])随重跑单调增长,属 analytics_query_log 追加型留痕的预期行为,不影响结论。


缺陷(3 个,报告侧;本轮不修)

缺陷 A —— core_trade.trade_date 列不存在(P1,用户可见)

根因(已直连库取证): core_trade 的时间列真名是 traded_at,没有 trade_date(15 列清单见 _raw/defect-A-direct-sql-proof.txt §1)。而以下三处都按 trade_date 写:

# 位置 写法
1 app/api/analyst.py:148 客户 dashboard 的 trades_30d 子查询
2 库模板 subscribe_amount_recent_days trade_type='subscribe' AND trade_date >= ...
3 库模板 customer_self_trade_count_recent customer_id=':customer_id' AND trade_date >= ...

三条用户可见后果:

  1. 客户「智能看数板」的指标恒为空对象。 GET /api/analyst/dashboard(customer)实测 {"cards":["我的持仓规模","近30日交易笔数","风险等级","盈亏概览"],"metrics":{}}。 注意连坐面:holdings 与 trades_30d 写在同一条 SELECT 里,trade_date 报错使整条语句失败 —— holdings 本可独立算出,也一并丢失。
  2. 静默失败被裸 except 掩盖。 app/api/analyst.py:170-171 的 except Exception: pass 把 1054 Unknown column 完全吞掉,HTTP 仍是 200,前端拿到的是"正常的空数据"。
  3. 问数页 9 个收录问句(推荐/热门按钮)中 3 个是坏按钮。 落到 status='escalate': 近30天申购金额总额 · 申购金额汇总(均属 featured=true 的模板)· 我近30天有多少笔交易。

探针设计上的一个坑(本轮新发现,已写进脚本注释): 第 3 个模板 customer_self_trade_count_recent 的 params_schema 声明了 extract='scope_customer',只在 customer 域渲染。按 analyst 身份探它时,参数解析失败 → 模板被跳过 → 静默回落真实 LLM → LLM 自行生成用 traded_at 的正确 SQL → status='success'。 即:单按 analyst 身份探测会漏报这一条。 脚本因此对 scope_customer 类模板(库中恰好 1 个)额外按 customer 身份复探(用例 8-03e-NN),才将其暴露。

复现: 三段命令与判据见 _raw/defect-A-direct-sql-proof.txt 文末;--only AN8 可独立复现该组 5 条 FAIL(已实测)。

缺陷 A2 —— schema 错误被误报为「查询超时」(P2,误导性提示)

根因: app/service/analytics_repo.py:16-18

def _is_timeout_error(exc: BaseException) -> bool:
    msg = str(exc).lower()
    return "timed out" in msg or "timeout" in msg or exc.__class__.__name__ in ("OperationalError",)

pymysql 对 1054(Unknown column)/ 1146(Unknown table)等 schema 错误同样抛 OperationalError,而该函数仅凭类名即判定超时。实测:_is_timeout_error(<1054 OperationalError>) -> True,于是 app/service/analyst_agent.py:228 走 EXEC_TIMEOUT 分支,用户看到 「查询超时(>10s)」。

为什么值得单独记账: 这不是文案瑕疵,而是把排障方向指向错误的地方 —— 提示引导人怀疑"库慢 / 加重试",而真实原因是查询根本没执行。缺陷 A 因此被这层错误分类又遮掩了一道。

缺陷 B —— /ops/metrics 返回形状不一致,列名丢失(P3)

app/api/analyst.py:270 返回 res["rows"][0],而 execute_readonly(analytics_repo.py:75)返回的是 rows: [list(r.values())] —— 即裸值列表。实测:

情形 实测返回
有数据 [216, 49](裸数组,无 total/blocked 键名)
无数据 {"total": 0, "blocked": 0}

后果: 同一接口的响应类型随数据量在 list / dict 之间跳变,消费方必须先做类型判断。数据本身正确(列序与 SELECT 一致),丢的是键名。 对照: 同一文件 :143 / :151 / :161 / :169 的四处 dashboard 分支都正确写了 dict(zip(res["columns"], res["rows"][0])) —— 唯独 ops_metrics 漏了,属实现不一致,不是设计取舍。 缓解事实: web/src 全仓无 /api/analyst/ops/metrics 引用(用例 11-03)—— 接口可用但无消费方,故当前不对前端造成实际故障。


待拍板(2 项,实测冻结,不计入 FAIL 分母)

# 事项 实测事实 为什么要拍板
C-1 「越权」在问数线有三种落地形态 ① HTTP 403 + 泛化 HTTP_ERROR(/dashboard·/template-prompts·/assets·/dict/ambiguity-check·/ops/metrics)② HTTP 200 + status="deny" + AUTH_403_ROLE(/chat·/interpret·/analyze·/query/{trace}/sample) 同一条线内两种拒绝风格并存。当前不构成功能缺陷:web/src/api/client.ts:44 只把 error_code 塞进 ApiError.code,无页面分支于此。但契约不一致本身就是债,须拍板统一到哪一种
C-2 /escalate 是 12 个 handler 中唯一不校验角色的 compliance(非问数白名单角色)调用实测 HTTP 200 ok=true。对照:其余 11 条均有 assert_analyst_query_access 或显式 analyst 角色校验 「问数失败者(含被拒角色)是否需要转人工通道」属设计意图问题,非实现 bug。已按「实测冻结」处理
C-3 403 的 error_code 被压成泛化码 register_error_handlers(app/utils/response.py:52)的白名单只有 404→NOT_FOUND / 405→METHOD_NOT_ALLOWED,其余一律 HTTP_ERROR —— 丢弃了 detail 里的具体语义(如 AUTH_403_ROLE) 前端无法据 error_code 区分"越权"与"参数错",只能匹配中文 message

编号纪律: 本包不使用裸 D1/D2/D3 —— docs/答辩/DEMO-功能测试流程.md 中该符号一号两义(第 64-65 行是问数线用例编号,第 79/89 行是缺陷编号)。本包一律写「问数线 D2」或带前缀的完整名。该文档缺陷记入横切包 XC。


覆盖矩阵(按组)

组 覆盖对象 PASS FAIL SKIP INFO
AN1 前置与契约冻结(/health·/api/ready·openapi 12 路由自检·7 账号登录与角色集合核对·基线加载) 7 0 0 1
AN2 鉴权与数据域(五角色→五域·越权探针·无 token) 17 0 0 2
AN3 POST /chat 主链路(四段齐全·只读红线·预警零增量·澄清路径) 16 0 0 3
AN4 POST /interpret 10 0 0 1
AN5 POST /analyze 9 0 0 1
AN6 GET /query/{trace_id}/sample 11 0 0 1
AN7 POST /escalate 5 0 0 1
AN8 GET /template-prompts + GET /dashboard 28 5 1 3
AN10 POST /dict/ambiguity-check 7 0 0 1
AN9 assets CRUD + publish 14 0 0 1
AN11 GET /ops/metrics 3 1 1 1
AN12 缓存 Tag 与模板 Tag(问数线 D1/D2) 9 0 0 2
AN13 只读红线终检(core 写表零增量·残留登记·前端消费面) 1 0 0 3
合计 137 6 2 21

执行顺序说明: ORDER 把 AN10 排在 AN9 之前 —— AN9 会写入沉淀资产,若先跑会污染 AN10 歧义检测的基线。

本包首次建立 HTTP 出进程 E2E 的 handler(10 条)

/dashboard · /interpret · /analyze · /query/{trace_id}/sample · /escalate · POST /assets · GET /assets · /assets/{kind}/{id}/publish · /dict/ambiguity-check · /ops/metrics (/chat · /template-prompts 此前已有进程内 TestClient 覆盖,本包补的是出进程真实性。)

其中 2 条的失败不是"没测过"而是"测了就坏": /dashboard(customer 分支恒空)· /ops/metrics(形状跳变)· 加上 3 个坏收录按钮,均属本次新建脚本才暴露的问题。

已验证转绿的红线(本包价值最高的正面证据)

红线 / 契约 取证方式 结果
「数据分析:仅 SELECT」 AN13-01 全程前后差值:core_trade 95→95 · core_holding 59→59 · core_share_lot 74→74 · core_convert_request 19→19 · core_convert_lot_detail 0→0 ✅ 五张写表 Δ0
「不处置预警」 AN3 调 /chat 前后 risk_alert 计数零增量 ✅
CT-001 P0 提权加固仍成立 AN1-06:对 customer actor 传入 roles=["analyst","risk_officer"],签发结果仍为 ['customer'](jwt_service.infer_roles 忽略调用方入参) ✅
跨域越权真被拦 AN2-10(INFO/软探针,非 PASS):customer 问全量聚合 → 实测 status='deny' error_code='AUTH_403_SCOPE' sql='' —— sql_guard 按 self 域拦下 ✅ 实测符合期望(但按纪律不计入 PASS 分母)
阻断路径留痕 AN11-02:越权后 analytics_query_log.blocked Δ+1 ✅
模板命中不调 LLM 且可缓存 AN12:二次同问 cache_hit=True + exec_ms=0 + template_hit=True;result_summary.source='template' ✅
openapi 路径基线 AN1:12 条 analyst 路由全在,路径总数 66(与计划基线一致,缺任一则退出码 2) ✅

Part B · 真浏览器(2026-09-13 补跑并归档)

04-analyst-market-advisor.mjs(本轮自仓库外收复进 scripts/e2e/):12 PASS / 0 FAIL / 1 INFO,退出码 0。 原生结果与截图见 _raw/;终端原始输出见横切包 2026-09-13-crosscut-e2e/_raw/04-analyst-market-advisor.stdout.txt。

覆盖点 实测
分析员首页有内容 全市场产品 14 只(上涨 3 / 下跌 9 / 平盘 2),数据截至 2026-09-11
问数工作台渲染看板指标 客户总数 33 · 总持仓规模 9834097.01 · 待处理预警数 37 · 口径字典资产数 4
问数提交并拿到结果 问「待处理预警按类型统计」→ 表格 alert_type/cnt:large_amount 2 · suitability 30 · aml 4 · freq_trade 1,SQL(4 行 · 121 ms)
结果无状态枚举泄漏(UI 已中文化) clean
资产沉淀页有内容 类型 dict/template 分组渲染正常,我的资产 列出 客户总数(template, published) 等
行情页有数据行 共 14 只产品;PROD-000001 现金宝货币 R1 1.0000 …
行情页无原始 ISO 时间 clean
行情搜索可过滤 14 -> 2
搜索无结果时不报错 placeholder=1
理财师首页有客户数/AUM 服务客户 12 · 持仓总市值 4,197,987.01 元
名下客户列表有数据 CUST-9527 客户·林** · CUST-1001 客户·王** … 各带「发起顾问对话」
名下客户页无原始 ISO clean

INFO(1 条): 问数结果表中的原始数据值不本地化 —— 这是设计如此(页面同时展示原始查询结果与 SQL,便于溯源),非缺陷。

与 Part A 的交叉印证(本包最值得记的一点)

Part B 的看板指标读数(客户总数 33 / 待处理预警 37 / 口径字典资产数 4)与 Part A 的缺陷 A 形成了互证: 缺陷 A 使客户分支的 /dashboard 恒为空对象(metrics={}),而 Part B 在分析员身份下读到的指标是非空且正确的 —— 因为分析员走的是 full 域的另一条 SQL 分支。 ⇒ 缺陷 A 的杀伤面精确限定在 customer 域的 dashboard 分支,并非整个 dashboard 接口。这条把缺陷 A 的影响面从"dashboard 坏了"收敛为"客户身份的 dashboard 坏了"。

仍然缺的(如实声明,勿夸大本节的覆盖)

# 项 状态
1 AnalystAssetsPage 的「提交沉淀」表单从未提交 只验了渲染(资产沉淀页有内容)。04-analyst-market-advisor.mjs 中无任何对该表单的 fill/click ⇒ POST /assets 的浏览器路径仍未覆盖(Part A 的 AN9 已从 HTTP 侧覆盖)
2 AnalystQueryPage 的数字未与库对账 Part B 只验"有结果",未做库侧交叉核对(Part A 的 AN8/AN13 已覆盖对账,且因此暴露缺陷 A)
3 AdvisorClientsDashboard 的 AUM 未与库对账 Part B 读得 4,197,987.01,未做库侧核对
4 AnalystQueryPage 的「解读/图表/转人工/抽样」四按钮 仅 Part A 从 HTTP 侧覆盖,未点过 UI

复现三连

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

# 1) 全量跑(真实 HTTP;退出码 0 全过 / 1 有 FAIL / 2 前置失败)
python scripts/dev/analyst_e2e_smoke.py --report D:/tmp/e2e-gap/an.md

# 2) 只复现缺陷 A(该组 5 条 FAIL,已实测)
python scripts/dev/analyst_e2e_smoke.py --only AN8

数据副作用: 本线不写 jinrong_core(AN13-01 取证 Δ0),无需还原。写侧仅落在 jinrong_agent 的追加型表:analytics_query_log(留痕,按语义不回滚)· audit_log(留痕)· analytics_metric_dict / analytics_few_shot(AN9 沉淀资产,键名带 E2E-AN-<runstamp> 前缀可过滤)。 本轮残留(AN9-10 登记): analytics_metric_dict +3 行 · analytics_few_shot +3 行(三轮跑批各 1,created_by=STAFF-20001)。未清理 —— 留着可验证"沉淀资产在库中可见且可发布"这条链路;如需清理,按 metric_key LIKE 'E2E-AN-%' 定位。

剩余风险 / 未覆盖

# 项 说明
1 Part B 已跑,但两处深挖仍未做 ✅ 04-analyst-market-advisor.mjs 已收复进仓库并跑通(12 PASS / 0 FAIL);⚠️ 但 AnalystAssetsPage 的「提交沉淀」表单仍未提交过、AnalystQueryPage 的数字仍未与库对账(详见「Part B」节末尾「仍然缺的」)
2 LLM 语义一律降级为软探针 AN3/AN4/AN5/AN10 中凡"回复须含某关键词"均记为 INFO、不计入 FAIL 分母(LLM 随机性,R2)
3 2 条 SKIP —— 两条都是缺陷的次生结果,不是环境问题 8-07a 本人持仓规模对账:metrics 为空对象(缺陷 A)导致无从取值;11-01b total 对账:响应是裸数组、无 total 键名(缺陷 B)导致无从取值。两者都是"设计如此"的 SKIP 而非假绿(expect_db_eq 对 got is None 一律 SKIP,绝不判 FAIL)。缺陷修复后这两条应自动转 PASS —— 可直接用作修复轮的验收信号。
4 仅 3 个角色覆盖负例 compliance / risk_manager 有覆盖;ops 与未登录的越权组合未穷举
5 analyst2 仅用于缓存跨主体用例 未测"两个 analyst 各自沉淀资产的可见性隔离"