docs(test): archive chat wait-state error-retry UX real-agent report
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
d03d9396e1
commit
4eaad5adb8
@@ -0,0 +1,220 @@
|
||||
# Web 对话等待态、错误提示与分型重试 UX(P1)真实 Agent 测试报告
|
||||
|
||||
## 一、测试背景 (Background)
|
||||
|
||||
用户在 Web 工作台发送闲聊/知识问答类消息后,此前长期存在 **10–15 秒无反馈** 的黑盒等待,且底层 `.catch()` 会在网络/超时失败时静默回落为模板成功回复,用户无法感知失败。Task 1–5 已完成如下 P1 实现:
|
||||
|
||||
- 后端 `error` 事件扩展为 `error_info`(`class`/`code`/`phase_id`/`upstream`/`request_id`/`actions`),并补齐 `intent_recognizing`/`dialogue_generating` 关键 phase(`internal/stream`、`internal/server/sse.go`)。
|
||||
- Web `App.tsx` 改为消费 `POST /api/v1/chat/stream` SSE,替换阻塞式 `fetch` + 人为 600ms 延迟;新增 `runStreamAttempt` 状态机(`pending → streaming_status → completed / auto_retrying(≤2, 1s/2s) / failed / cancelled`),删除旧的静默成功模板回落。
|
||||
- `web/src/api/chatStream.ts`、`chatRetry.ts` 提供 SSE 解析、`shouldAutoRetry`/`autoRetryDelayMs`/`fallbackErrorClass` 等纯函数,并配有单元测试。
|
||||
|
||||
本报告是 Task 6:在**真实环境**(`pi-agent` 容器 + 真实 MiniMax-M3 模型)下对上述实现做黑盒 E2E 验收,同时执行全量回归测试,严禁使用 stub 输出作为验收依据。
|
||||
|
||||
## 二、测试目标 (Test Objectives)
|
||||
|
||||
1. 验证后端服务可正常构建、启动,`/api/v1/health` 返回 `agent_backend: pi-agent`。
|
||||
2. 验证 Case A:闲聊问答通过 **SSE**(非阻塞 `chat`)真实调用 pi-agent + MiniMax-M3,确认 `phase`(`intent_recognizing`/`dialogue_generating`)、`delta`、`done` 事件齐全,首个事件延迟 **≤1s**,回复内容为真实、具体、非模板占位文本。
|
||||
3. 验证 Case B:错误路径下 `event: error` 携带 `error_info` 的可行性;若无法安全诱发,退而以死端口验证客户端网络失败分类预期,并交叉引用 Task 3 已覆盖的单元测试。
|
||||
4. 回归 Go 测试(`internal/stream`、`internal/chat`、`internal/server`)与 Web 测试(`chatStream.test.ts`、`chatRetry.test.ts`)全部通过或标注已知环境相关失败原因。
|
||||
5. 报告按 `.cursor/rules/test-reports-standard.mdc` 固定结构归档至 `.doc/test-reports/`。
|
||||
|
||||
## 三、测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
**环境**
|
||||
|
||||
| 项 | 值 |
|
||||
| :--- | :--- |
|
||||
| OS | macOS 26.6.2 (Darwin 25.6.0, arm64) |
|
||||
| Go | go1.27.1 darwin/arm64 |
|
||||
| 分支 / commit | `dev` @ `56fb3180c2d6eeca1f49b8c0d92b31f25708496c` |
|
||||
| 后端服务 | `./bin/server`(重新 `go build` 产出),监听 `127.0.0.1:8080` |
|
||||
| Agent 后端 | `AGENT_BACKEND=pi-agent`(`.env`,未打印密钥) |
|
||||
| pi-agent 容器 | `shuimu-pi-agent-dev`(`127.0.0.1:18865`,`healthy`) |
|
||||
| 上下文库 | `shuimu-context-pg-dev`(`127.0.0.1:5433`,`healthy`) |
|
||||
| 共享知识库 | `shuimu-kb-shared`(`127.0.0.1:5432`,`healthy`) |
|
||||
|
||||
**测试层级**
|
||||
|
||||
1. 服务自检:`/api/v1/health`。
|
||||
2. 真实 Agent 黑盒 E2E:`curl -N` 直连 SSE,逐行打印 `[t=+Ns]` 相对时间戳,捕获真实控制台输出。
|
||||
3. 受控故障注入:临时 `docker stop/start shuimu-pi-agent-dev`,观察 chat/stream 行为;辅以死端口 `curl` 验证客户端网络失败特征。
|
||||
4. 自动化回归:`go test` 三包 + Web `node:test` 两文件。
|
||||
|
||||
**核心用例矩阵**
|
||||
|
||||
| 用例 | 输入 | 验证点 |
|
||||
| :--- | :--- | :--- |
|
||||
| Case A | `请用两句话解释逆压电效应。`(`session_id=e2e-wait-retry`) | phase/status/delta/done 齐全;首事件 <1s;回复非模板 |
|
||||
| Case B-1 | 同上问题,`pi-agent` 容器已停止 | 观察是否触发 `event: error` |
|
||||
| Case B-2 | 同上问题,目标端口为未监听的 `8099` | curl 层网络失败分类(连接被拒绝) |
|
||||
|
||||
## 四、预期结果 (Expected Results)
|
||||
|
||||
- Case A:`event: phase`(`intent_recognizing`)应在请求发出后 **<1s** 到达;随后 `phase`(`dialogue_generating`)、`status`(意图/分支判定)、`delta`(真实生成的自然语言正文)、`done`(含 `duration_ms`/`total_events`)依次出现;正文应具体回答"逆压电效应",不得是通用模板占位句。
|
||||
- Case B:若成功诱发真实 transient 故障,应看到 `event: error`,`data.error_info` 含 `class`(如 `transient`)、`code`、`phase_id`、`upstream`、`request_id`、`actions`(含 `retry`)。若诱发路径因既有设计(见五节发现)不产生 `error` 事件,则应退回死端口方案,证明连接层失败(`curl` 退出码 7 / Connection refused),并交叉引用 `chatRetry.test.ts` 中 `fallbackErrorClass: network → transient` 单测作为分类逻辑的既有验证。
|
||||
- 回归:`internal/stream`、`internal/chat` 全绿;`internal/server` 除已知环境相关全生命周期用例外全绿;Web 两个测试文件全绿。
|
||||
|
||||
## 五、实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 0. 环境准备(Blocking issue 修复)
|
||||
|
||||
Controller 报告 `:8080` 健康检查为 DOWN。执行:
|
||||
|
||||
```
|
||||
$ go build -o ./bin/server ./cmd/server # 编译成功,无输出
|
||||
$ set -a && source .env && set +a && ./bin/server &
|
||||
$ curl -s -i http://127.0.0.1:8080/api/v1/health
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
{"status":"ok","env":"dev","agent_backend":"pi-agent","version":"v1.0","shared_kb_port":5432}
|
||||
```
|
||||
|
||||
**判定:PASS** — 服务重建并启动成功,`agent_backend=pi-agent` 确认走真实 Agent 路径。`.env` 中 `AGENT_BACKEND=pi-agent`、`MINIMAX_*`、`PI_AGENT_*` 均已配置(值未打印)。`shuimu-pi-agent-dev` 容器 `healthy`(`docker ps` 确认)。
|
||||
|
||||
---
|
||||
|
||||
### Case A — 闲聊等待态 SSE 真实链路
|
||||
|
||||
**输入 (Input):**
|
||||
|
||||
```bash
|
||||
curl -N -s -D - -X POST http://127.0.0.1:8080/api/v1/chat/stream \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"session_id":"e2e-wait-retry","message":"请用两句话解释逆压电效应。","user_id":1,"project_id":3}'
|
||||
```
|
||||
|
||||
**实际输出 (Actual Output,真实控制台,附相对时间戳):**
|
||||
|
||||
```
|
||||
[t=0.015s] event: phase
|
||||
[t=0.024s] data: {"id":"evt_..._1","type":"phase","phase":{"phase_id":"intent_recognizing","message":"正在研判您的意图与项目上下文...","role":"coordinator"}}
|
||||
|
||||
[t=0.038s] event: status
|
||||
[t=0.046s] data: {"...","status":{"intent":"CASUAL_CHAT","branch":"AMBIGUOUS_FALLBACK","current_stage":"V0.1_triage","message":"识别分支: AMBIGUOUS_FALLBACK (置信度: 20%)"}}
|
||||
|
||||
[t=0.059s] event: phase
|
||||
[t=0.065s] data: {"...","phase":{"phase_id":"dialogue_generating","message":"正在整理回复...","role":"assistant","progress":40}}
|
||||
|
||||
[t=0.077s] event: status
|
||||
[t=0.085s] data: {"...","status":{"intent":"CASUAL_CHAT",...,"message":"意图判定: CASUAL_CHAT (置信度: 20%)"}}
|
||||
|
||||
[t=0.098s] event: phase
|
||||
[t=0.104s] data: {"...","phase":{"phase_id":"dialogue_generating","message":"正在生成项目助手回复...","role":"assistant","progress":60}}
|
||||
|
||||
[t=14.218s] event: delta
|
||||
[t=14.227s] data: {"...","delta":"好的,我帮您简单梳理一下:逆压电效应是指对压电材料施加外电场时,材料内部的电偶极子沿电场方向发生取向与位移,从而产生与电场强度成比例的机械形变(应变);它是正压电效应的逆过程——正压电效应是\"机械应力→电荷\",逆压电效应则是\"电场→机械位移\",也正是超声换能器、压电致动器等医用器件实现电-机能量转换的工作基础。\n\n如果您接下来是想把它对应到咱们项目里的某个具体部件(比如超声探头、压电驱动模组)来理解机理或排查问题,告诉我场景,我可以再帮您展开一层。"}
|
||||
|
||||
[t=14.240s] event: done
|
||||
[t=14.247s] data: {"...","done":{"session_id":"e2e-wait-retry","duration_ms":14202,"total_events":6}}
|
||||
```
|
||||
|
||||
响应头:`X-Request-Id: req_1788945422397553000`,`Content-Type: text/event-stream`,`Cache-Control: no-cache`。
|
||||
|
||||
**评价判定 (Evaluation / Verdict):**
|
||||
|
||||
| 验收点 | 结果 |
|
||||
| :--- | :--- |
|
||||
| 首个 `event: phase`(`intent_recognizing`)延迟 ≤1s | **PASS**(0.015s) |
|
||||
| `dialogue_generating` phase 出现 | **PASS**(0.065s / 0.104s,两次进度推进 40%→60%) |
|
||||
| `event: delta` 出现且为整段正文 | **PASS**(单次整段,非逐字) |
|
||||
| `event: done` 收尾并含耗时统计 | **PASS**(`duration_ms=14202`,`total_events=6`) |
|
||||
| 回复是否为真实、具体、非模板占位 | **PASS** — 正确解释了"逆压电效应"的物理机制(电偶极子取向位移→机械应变),并与正压电效应做对比,末尾自然关联项目部件,语气符合"项目助手"人设,非缓存/模板套话 |
|
||||
| 无假成功模板 | **PASS**(本用例路径真实命中 pi-agent,`reply_source=pi-agent`) |
|
||||
|
||||
**性能与耗时 (Duration & Latency):** 首个等待态事件 15ms(远优于 ≤1s 验收线);phase 推进在 100ms 内完成;真实 MiniMax-M3 生成耗时 **14.1s**(`14.218s - 0.104s`),属于真实大模型推理延迟,与设计文档"10–15 秒无反馈"背景描述一致——P1 的价值正是在这 14s 内提供可见等待态文案而非黑盒空白。
|
||||
|
||||
**Case A 结论:PASS。**
|
||||
|
||||
---
|
||||
|
||||
### Case B — 错误路径 + 客户端网络失败分类
|
||||
|
||||
**尝试 1(受控诱发 transient 故障):** 临时停止真实 pi-agent 容器,复用相同问题发起 SSE 请求:
|
||||
|
||||
```bash
|
||||
$ docker stop shuimu-pi-agent-dev # Exited (137)
|
||||
$ curl -N -s -D - -X POST http://127.0.0.1:8080/api/v1/chat/stream \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"session_id":"e2e-wait-retry-err","message":"请用两句话解释逆压电效应。","user_id":1,"project_id":3}'
|
||||
```
|
||||
|
||||
**实际输出:** 收到完整 `phase`/`status`/`delta`/`done` 序列,**未出现 `event: error`**;`done.duration_ms=9`(毫秒级,非真实生成耗时),`delta` 内容是一段与"逆压电效应"无关的通用推进建议话术(V0.5 方案框架选项)。
|
||||
|
||||
**根因排查:** 阅读 `internal/coordinator/dialogue_llm.go` 与 `docs/superpowers/specs/2026-09-09-dialogue-llm-casual-ambiguous-design.md` 确认,这是**既有、独立的 Phase A 设计行为**(非本次 P1 引入的缺陷):`generateDialogueReply` 对 `CASUAL_CHAT`/`AMBIGUOUS_FALLBACK` 分支调用 pi-agent 失败时,按该设计文档第 26 行 "失败回退原模板,`ReplySource=template`" 的既定契约静默降级为规则模板,由 `internal/coordinator/engine.go:301/748` 处的 `ReplySourceTemplate` 兜底吸收,不会传播到 `sse.go` 的 `SendMessageStream` 错误返回路径,因此该场景**不产生** `error` 事件。这与 P1 设计"失败不得写入成功模板"的原则存在语义交叉,但适用范围不同:P1 该原则约束的是 `chat/stream` 层面的 transient/网络/超时错误(`ClassifyError`/`EmitError` 覆盖的路径,例如 `handleRunTaskStream`/`handleChatStream` 中 `sess.SendMessageStream` 返回非 nil error 时),而闲聊对话生成的模板兜底属于 2026-09-09 单独设计并验收通过的 Phase A 契约,两者不是同一交付边界,因此**不计为本次 P1 缺陷**,但记录为后续需澄清的边界(见第七节)。
|
||||
|
||||
**尝试 2(文档要求的替代方案:死端口验证客户端网络失败分类):**
|
||||
|
||||
```bash
|
||||
$ curl -N -s -v -X POST http://127.0.0.1:8099/api/v1/chat/stream \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"session_id":"e2e-dead-port","message":"test","user_id":1,"project_id":3}' \
|
||||
--max-time 5
|
||||
* Trying 127.0.0.1:8099...
|
||||
* connect to 127.0.0.1 port 8099 from 127.0.0.1 port 61315 failed: Connection refused
|
||||
* Failed to connect to 127.0.0.1 port 8099 after 0 ms: Couldn't connect to server
|
||||
* Closing connection
|
||||
CURL_EXIT_CODE:7
|
||||
```
|
||||
|
||||
这重现了 Web `fetch`/`chatStream.ts` 在服务不可达时会捕获的 `TypeError`/网络异常特征。按设计文档 4.3/4.5 节及 `App.tsx` 的 `buildNetworkErrorInfo` 实现,此类异常应在前端被合成为 `class=transient` 的 `errorInfo` 并进入自动重试路径(1s→2s,≤2 次)。该分类逻辑已在 Task 3 `chatRetry.test.ts` 的 `fallbackErrorClass: network → transient, workspace → resumable` 单测中覆盖,本次回归重新执行该测试并通过(见第六节)。
|
||||
|
||||
**评价判定 (Evaluation / Verdict):**
|
||||
|
||||
| 验收点 | 结果 |
|
||||
| :--- | :--- |
|
||||
| 真实环境下诱发 pi-agent 故障并观察行为 | **完成**(尝试 1,发现既有 Phase A 模板降级契约,非本 P1 缺陷) |
|
||||
| `event: error` + `error_info` 结构 | **未在闲聊路径复现**(原因见上,超出本任务修复范围) |
|
||||
| 客户端网络失败可被分类为 transient(死端口替代方案) | **PASS**(连接被拒绝,`curl` exit 7;对应前端分类单测通过) |
|
||||
| Web 自动重试单测覆盖 | **PASS**(`chatRetry.test.ts` 6/6,见第六节) |
|
||||
|
||||
**Case B 结论:PASS(有条件)** — 未能在真实闲聊路径下直接观测到 `event:error`(根因为既有独立设计契约,非本次 P1 引入的回归),已用文档允许的死端口替代方案完成客户端网络失败分类的真实验证,并交叉核实前端分类/自动重试单测。建议后续澄清"闲聊模板降级"与"P1 无假成功模板"两个设计的边界(详见第七节)。
|
||||
|
||||
## 六、自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
### Go 回归(`internal/stream`、`internal/chat`、`internal/server`)
|
||||
|
||||
```
|
||||
$ go test ./internal/stream/ ./internal/chat/ ./internal/server/ -count=1 -v
|
||||
...
|
||||
ok chat-agent-system/internal/stream 1.409s (6 passed)
|
||||
ok chat-agent-system/internal/chat 2.376s (8 passed)
|
||||
--- FAIL: TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E (57.08s)
|
||||
piagent_e2e_test.go:145: ExecuteStage V0.1 failed: docgov: document is locked, direct modification prohibited
|
||||
FAIL chat-agent-system/internal/server 60.502s (20 passed / 1 failed)
|
||||
```
|
||||
|
||||
**汇总:34 passed / 1 failed(35 total)。**
|
||||
|
||||
失败用例 `TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E` 为**已知环境相关失败**:该用例复用 `data/dev` 工作区真实 docgov 文档状态,此前多轮全生命周期真实 E2E(`.doc/test-reports/2026-09-08-piagent-full-lifecycle-multirole-test-report.md` 等)已将同一受控文档推进为"已认领/锁定"状态,本次运行时 `ExecuteStage V0.1` 试图直接修改已锁定文档而报错,属于**跨轮次共享 dev 数据状态**导致的环境残留问题,与本次 P1 chat 等待态/错误/重试改动(`internal/stream`、`internal/server/sse.go`、`internal/chat`)**无代码路径重叠**(该失败发生于 `bizdev.ExecuteStage`/`docgov` 锁校验,Case A/B 验证的 `handleChatStream`/`SendMessageStream` 路径未受影响,且本报告 Case A/B 及其余 20 个 `internal/server` 用例,含 `TestServer_ChatSSEStream`、`TestDocEditLease_HTTPConcurrencyGates`,均通过)。按任务说明"跳过已知环境相关 E2E 失败并注明原因"处理,不计入 P1 验收范围。
|
||||
|
||||
### Web 测试(`chatStream.test.ts` + `chatRetry.test.ts`)
|
||||
|
||||
```
|
||||
$ cd web && npx tsx --test src/api/chatStream.test.ts src/api/chatRetry.test.ts
|
||||
✔ shouldAutoRetry: transient only, max 2 (0.319292ms)
|
||||
✔ autoRetryDelayMs: 1s then 2s (0.052166ms)
|
||||
✔ fallbackErrorClass: network → transient, workspace → resumable (0.048583ms)
|
||||
✔ parseSSEChunk: parses event and data lines (0.345958ms)
|
||||
✔ parseSSEChunk: keeps incomplete frame in rest (0.149583ms)
|
||||
✔ parseSSEChunk: flush parses final frame without trailing delimiter (0.060458ms)
|
||||
ℹ tests 6
|
||||
ℹ pass 6
|
||||
ℹ fail 0
|
||||
```
|
||||
|
||||
**汇总:6/6 passed。**
|
||||
|
||||
## 七、测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
**结论:P1「Web 对话等待态、错误提示与分型重试 UX」达到真实 Agent 环境交付标准。**
|
||||
|
||||
- Case A 在真实 `pi-agent` + MiniMax-M3 链路下完整验证:≤1s 等待态占位、`phase`/`status` 真实进度、整段 `delta` 正文(非模板、内容具体正确)、`done` 收尾统计,全部达标。
|
||||
- Case B 因既有独立设计(闲聊模板降级契约)无法在该特定路径下直接复现 `event:error`,已用任务文档允许的死端口替代方案完成客户端网络失败分类真实验证,并交叉核实自动重试相关单测(Task 3)全部通过。
|
||||
- 回归测试 34/35(Go)+ 6/6(Web)通过,唯一失败为与本次改动无关的已知环境残留(`docgov` 文档跨轮次锁定状态)。
|
||||
|
||||
**后续优化建议:**
|
||||
|
||||
1. **澄清模板降级与错误可见性的边界**:建议产品/设计侧明确"闲聊模板降级"(`ReplySource=template`)是否应在**多次连续失败**时也升级为可见的 `error_info`(例如追加 `reply_degraded` 标记透传给前端做轻量提示),避免用户长期无感知地收到与本轮问题无关的兜底话术。
|
||||
2. **补充真实 transient 故障的会话级集成测试**:当前 `internal/stream`/`internal/chat` 单测覆盖 `ClassifyError`/`EmitError` 的构造逻辑,但缺少一个"真实诱发 `sess.SendMessageStream` 返回 error → SSE 层输出 `event:error`"的集成用例(例如通过可注入的 stage 失败路径),建议 P2 阶段补齐。
|
||||
3. **`docgov` 环境残留清理**:建议为真实 E2E 测试增加独立/临时工作区或测后重置脚本,避免 `data/dev` 状态跨轮次污染导致非相关用例失败,影响回归可读性。
|
||||
4. P2/P3(`PhaseEventStrip` live 化、`resumable`/`fatal` 动作接线、token 级 `delta`)按原分期计划推进,本报告不涉及其验收。
|
||||
Reference in New Issue
Block a user