- 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.
12 KiB
12 KiB
测试日志:问数线(数据分析 Agent)真实 HTTP 端到端补测
编号接续:
TEST-LOG-2026-09-11-AN-001(域 RBAC,进程内 TestClient)→AN-002(模板短语)→ 本文件AN-003(出进程真实 HTTP 全路由)。
1. 文档元数据
| 项 | 值 |
|---|---|
| 测试记录编号 | TEST-AN-003 |
| 缺陷/变更标题 | 问数线 12 个 handler 的真实 HTTP 端到端补测;暴露缺陷 A / A2 / B |
| 文档版本 | v1.0 |
| 创建日期 | 2026-09-13 |
| 关联分支 | merger |
| 关联拍板/TODO | 本轮七项拍板(口径 2「只报告不修」· 口径 7「一线一包」);docs/memory/TODO.md E2E 覆盖缺口条目 |
| 风险等级 | MEDIUM(缺陷 A 用户可见;但业务库零写入、无数据损坏风险) |
| 缺陷类型 | ① 数据库列名错误(缺陷 A)② 异常分级失真(缺陷 A2)③ 响应契约不一致(缺陷 B) |
| 发现阶段 | 端到端补测(本轮新建脚本首次执行) |
2. 组织与责任
| 项 | 值 |
|---|---|
| 所属模块 | 数据分析 Agent(问数线)—— app/api/analyst.py · app/service/analyst_agent.py · app/service/analytics_repo.py · jinrong_agent 模板数据 |
| 模块负责人 | (待指定) |
| 发现人 | 本轮 E2E 补测 |
| 修改人 | 无(本轮口径:只报告不修) |
| 测试执行人 | 本轮 E2E 补测 |
| 评审人 | 待评审 |
3. 缺陷描述
3.1 缺陷 A —— core_trade.trade_date 列不存在
| 项 | 内容 |
|---|---|
| 缺陷编号 | DEF-AN-003-A |
| 严重等级 | P1 |
| 复现步骤 | 1. python scripts/dev/analyst_e2e_smoke.py --only AN82. 看 8-03b-07 / 8-03b-08 / 8-03e-04 / 8-07c3. 直连库复核: SHOW COLUMNS FROM core_trade(见 _raw/defect-A-direct-sql-proof.txt) |
| 期望 | ① 客户 dashboard 返回 metrics 含 holdings 与 trades_30d;② 问数页 9 个收录问句无一落到 escalate |
| 实际 | ① metrics={}(恒空对象)② 3 条落到 status='escalate' code='EXEC_TIMEOUT':近30天申购金额总额 · 申购金额汇总 · 我近30天有多少笔交易 |
| 根因 | core_trade 的时间列真名是 traded_at,无 trade_date(15 列实测清单见取证文件 §1)。三处按 trade_date 写:app/api/analyst.py:148(客户 dashboard 的 trades_30d 子查询)、库模板 subscribe_amount_recent_days、库模板 customer_self_trade_count_recent。执行即 1054 Unknown column 'trade_date' in 'where clause'。放大器 1(连坐): holdings 与 trades_30d 在同一条 SELECT 内,列错误使整条语句失败 → 本可独立算出的 holdings 一并丢失。放大器 2(掩盖): app/api/analyst.py:170-171 的 except Exception: pass 吞掉异常,HTTP 仍 200,前端拿到"正常的空数据"。放大器 3(漏检): customer_self_trade_count_recent 声明 extract='scope_customer',只在 customer 域渲染;按 analyst 身份探它会因参数解析失败而跳过模板 → 回落真实 LLM,LLM 自行生成用 traded_at 的正确 SQL,于是 status='success',缺陷被掩盖。单身份探测会漏报此条。 |
| 修复方案(本轮不实施) | 四选一,须先拍板:① 改三处代码/模板为 traded_at(最小改动,但需确认 traded_at 的语义与 trade_date 的 30 天窗口口径一致);② 在 core_trade 加 trade_date 生成列/视图(改动 DDL,属高风险项,须确认);③ 客户 dashboard 拆成两条独立 SELECT(消除连坐,但不治根);④ 问数线改走 traded_at 并给模板加列名校验。另须一并处置放大器 2(裸 except 改为记日志/上抛)与放大器 3(模板参数解析失败应显式降级并留痕,而非静默回落 LLM)。 |
3.2 缺陷 A2 —— schema 错误被误报为「查询超时」
| 项 | 内容 |
|---|---|
| 缺陷编号 | DEF-AN-003-A2 |
| 严重等级 | P2 |
| 复现步骤 | python scripts/dev/analyst_e2e_smoke.py --only AN8,看 8-03d;或执行取证文件 §3 的直连探针 |
| 期望 | 列不存在这类 schema 错误应报 EXEC_ERROR(并带原始 SQL 错误) |
| 实际 | error_code='EXEC_TIMEOUT',HTTP 200,用户看到**「查询超时(>10s)」** |
| 根因 | app/service/analytics_repo.py:16-18 的 _is_timeout_error() 仅凭异常类名判定: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 分支。 |
| 危害说明 | 非文案瑕疵。该提示把排障方向指向错误的地方("库慢 / 加重试"),而真实原因是查询根本没执行;且它把缺陷 A 又遮掩了一道。 |
| 修复方案(本轮不实施) | 收窄判定:只认真正的超时(pymysql.err.OperationalError 的 args[0] ∈ 超时码 2013/2006/1205,或 msg 含 timeout);把 schema 错误(1054/1146/1064 等)归入 EXEC_ERROR 并回传原始 SQL 错误信息。 |
3.3 缺陷 B —— /ops/metrics 返回形状不一致,列名丢失
| 项 | 内容 |
|---|---|
| 缺陷编号 | DEF-AN-003-B |
| 严重等级 | P3 |
| 复现步骤 | python scripts/dev/analyst_e2e_smoke.py --only AN11,看 11-01a(实测 list [216, 49]) |
| 期望 | 恒返回 {"total": <int>, "blocked": <int>} |
| 实际 | 有数据时返回裸数组 [216, 49];无数据时返回 {"total": 0, "blocked": 0} |
| 根因 | app/api/analyst.py:270 返回 res["rows"][0],而 execute_readonly(analytics_repo.py:75)返回 rows: [list(r.values())] —— 即裸值列表,列名在传递中丢失。同文件 :143/:151/:161/:169 四处 dashboard 分支都正确写了 dict(zip(res["columns"], res["rows"][0])),唯独 ops_metrics 漏了 → 属实现不一致,非设计取舍。数据本身正确(列序与 SELECT 一致)。 |
| 缓解事实 | web/src 全仓无 /api/analyst/ops/metrics 引用(用例 11-03)—— 接口可用但无消费方,当前不对前端造成实际故障。 |
| 修复方案(本轮不实施) | 补 dict(zip(res["columns"], res["rows"][0])),与 dashboard 分支写法对齐。一行改动。 |
4. 变更清单
| 类型 | 路径 | 说明 |
|---|---|---|
| 新增 | scripts/dev/analyst_e2e_smoke.py |
问数线真实 HTTP E2E,13 组 166 条,约 1.5k 行 |
| 新增 | docs/memory/tests/2026-09-13-analyst-e2e/README.md · 本日志 · _raw/ |
本包 |
| 业务代码 | —— | 本轮未改动任何 app/**(口径 2「只报告不修」) |
5. 测试执行记录
| 序号 | 类型 | 用例/命令 | 执行人 | 结果 | 证据 |
|---|---|---|---|---|---|
| 1 | 前置 | curl -s http://127.0.0.1:8000/api/ready |
本轮 | PASS | ok=true degraded=false redis=true |
| 2 | 契约冻结 | AN1:openapi 含 12 条 analyst 路由、路径总数 = 66 | 本轮 | PASS | _raw/an-canonical.md AN1 组 |
| 3 | 鉴权矩阵 | AN2:五角色→五域 + 越权探针 + 无 token | 本轮 | PASS | 同上 AN2 组(17 PASS) |
| 4 | 只读红线 | AN13-01:core 五表前后差值 | 本轮 | PASS | core_trade 95→95 等五表 Δ0 |
| 5 | 预警红线 | AN3:/chat 前后 risk_alert 零增量 |
本轮 | PASS | AN3-05(3-05a 预警问句 200 · 3-05b 计数与库中事实一致) |
| 6 | 提权加固 | AN1-06:customer actor 传 roles=["analyst","risk_officer"] |
本轮 | PASS | 签发仍为 ['customer'] |
| 7 | 跨域越权 | AN2-10:customer 问全量聚合 | 本轮 | PASS | status='deny' code='AUTH_403_SCOPE' |
| 8 | 缓存 | AN12:二次同问 | 本轮 | PASS | cache_hit=True exec_ms=0 template_hit=True |
| 9 | 缺陷取证 | AN8:--only AN8 |
本轮 | FAIL ×5 | 缺陷 A(4 表现)+ A2(1) |
| 10 | 缺陷取证 | AN11:--only AN11 |
本轮 | FAIL ×1 | 缺陷 B |
| 11 | 全量跑批 | python scripts/dev/analyst_e2e_smoke.py(连续 3 轮) |
本轮 | 137 PASS / 6 FAIL / 2 SKIP / 21 INFO,退出码 1 | _raw/an-canonical.*;三轮 FAIL 集合一致 |
| 12 | 直连库取证 | _raw/defect-A-direct-sql-proof.txt 三段命令 |
本轮 | 全部复现 | 1054 异常原文 + _is_timeout_error -> True |
FAIL 明细(6 条,全部为缺陷表现,无脚本自身 bug):
| 用例 | 实测 | 归属 |
|---|---|---|
8-03b-07 收录问句可答 · 近30天申购金额总额 |
status='escalate' code='EXEC_TIMEOUT' |
缺陷 A |
8-03b-08 收录问句可答 · 申购金额汇总 |
status='escalate' code='EXEC_TIMEOUT' |
缺陷 A |
8-03e-04(as customer) 收录问句可答 · 我近30天有多少笔交易 |
status='escalate' code='EXEC_TIMEOUT' |
缺陷 A |
8-07c 客户看数板 metrics 应非空 |
metrics={} |
缺陷 A |
8-03d 失败原因不被误报为 EXEC_TIMEOUT |
codes=['EXEC_TIMEOUT'] |
缺陷 A2 |
11-01a 返回 total/blocked 两字段 |
list [216, 49] |
缺陷 B |
首轮曾出现的 5 条 FAIL 中有 4 条是脚本自身缺陷,已修正下述,不属被测系统问题(记录在案以示区分): ①
AN4-04误把 advisor 当越权角色 —— advisor 在analyst_auth_adapter._ROLE_PRIORITY内、域assigned被授予,属合法调用方(问数线宽于 chat 矩阵);已改用 compliance 作负例,并保留 advisor 作正例。 ②AN8-02期望"模板数"而list_published_prompts按match_phrase展开(7 模板 → 9 项);已改为同时断言"条目数 == 短语总数"与"去重 key 数 == 有短语的模板数"。 ③④expect_db_eq用str()直比Decimal("7297060.00")与float 7297060.0,产生纯表示层假 FAIL;已加数值归一化。 ⑤AN11-01a是真缺陷(缺陷 B),保留。
6. 数据准备
| 数据集 | 是否重灌 | 操作 |
|---|---|---|
jinrong_core(5 张写表) |
否 | 本线不写入;AN13-01 前后差值取证 Δ0,无需还原 |
jinrong_agent.analytics_query_template |
否 | 只读(加载发布模板池与短语数作为对账基线) |
jinrong_agent.analytics_query_log |
否 | 追加型留痕,按语义不回滚 |
jinrong_agent.analytics_metric_dict / analytics_few_shot |
否 | AN9 沉淀资产,键名带 E2E-AN-<runstamp> 前缀;三轮跑批各新增 1 行,残留已登记(AN9-10) |
jinrong_agent.audit_log |
否 | 追加型留痕,永不回滚 |
7. 结论
| 项 | 结论 |
|---|---|
| 需求覆盖 | 问数线 12/12 个 handler 全部建立真实 HTTP 出进程 E2E。其中 10 条为本轮首次(此前仅有 /chat 与 /template-prompts 的进程内 TestClient 覆盖)。13 组 166 条:137 PASS / 6 FAIL / 2 SKIP / 21 INFO。 |
| 回归 | 本线不涉及既有脚本改动,不影响 advisor_e2e_smoke.py / risk_e2e_smoke.py 回归基线。未改动任何业务代码。 |
| 缺陷 | 3 个(A / A2 / B),6 条 FAIL 表现。均只报告不修。其中缺陷 A 为 P1、用户可见(客户看数板恒空 + 3 个推荐问句是坏按钮)。 |
| 正面证据 | 7 条红线/契约已取证转绿:只读红线(core 五表 Δ0)· 不处置预警(risk_alert Δ0)· CT-001 P0 提权加固仍成立 · 跨域越权真被拦(AUTH_403_SCOPE)· 阻断路径留痕(Δ+1)· 模板缓存 Tag · openapi 基线 66。 |
| 剩余风险 | ① Part B 浏览器未跑(AnalystAssetsPage 表单从未提交、AnalystQueryPage 数字从未对账)② LLM 语义类一律软探针、不计入分母 ③ 2 条 SKIP 是缺陷次生结果,修复后应自动转 PASS,可作修复轮验收信号 ④ ops 角色与未登录组合的越权未穷举。 |
| 建议人工再验 | ① 缺陷 A 修复后,重点人工复核 traded_at 与"30 天窗口"的口径是否等价(不只是列名替换)② 缺陷 A 的放大器 3:模板参数解析失败静默回落 LLM 是否应显式降级 ③ 待拍板 C-1/C-2/C-3(拒绝形态不统一 · /escalate 无角色校验 · 403 错误码被压成 HTTP_ERROR)。 |
| 可复现性 | 连续三轮跑批 FAIL 集合完全一致;--only AN8 可独立复现缺陷 A 全部 5 条(已实测)。 |
8. 签核 / 修订历史
| 版本 | 日期 | 修改人 | 说明 |
|---|---|---|---|
| v1.0 | 2026-09-13 | 本轮 E2E 补测 | 首版。Part A 完成(137/6/2/21,退出码 1);Part B 待 Phase 4 追加 |