docs(test): strengthen chat wait-error-retry report with web E2E evidence

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WeiCHEN
2026-09-09 17:41:02 +08:00
co-authored by Cursor
parent 4aa839c5d1
commit a964899f51
5 changed files with 116 additions and 14 deletions
@@ -10,6 +10,8 @@
本报告是 Task 6:在**真实环境**(`pi-agent` 容器 + 真实 MiniMax-M3 模型)下对上述实现做黑盒 E2E 验收,同时执行全量回归测试,严禁使用 stub 输出作为验收依据。
> **复审强化说明(本次更新)**:Task 6 复审发现上一轮 Case B 仅用 `curl` 死端口方案验证,未在真实 Web 浏览器路径中直接观察等待态/失败/重试 UI 行为,判定被要求收紧为"PASS 仅当 Web 路径被证实,否则 FAIL/INCOMPLETE"。本次更新使用 `cursor-ide-browser` MCP 驱动真实浏览器(含真实账号登录、CDP 网络层阻断模拟离线、恢复网络后点击重试)补齐 Case A-Web / Case B-Web 的完整证据链(见 Case A / Case B-Web 小节及截图),并将 Go/Web 回归范围扩大为全量 `go test ./...` 与 `npm test`(见第六节 6.3/6.4)。
## 二、测试目标 (Test Objectives)
1. 验证后端服务可正常构建、启动,`/api/v1/health` 返回 `agent_backend: pi-agent`。
@@ -158,20 +160,65 @@ 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):**
---
### Case B-Web — 真实浏览器 E2E 复审强化(本轮新增,Task 6 复审要求)
> **背景**:上一轮验收仅用 `curl` 死端口方案交叉验证了分类逻辑,未在**真实浏览器工作台**中直接观察等待态/失败气泡/重试按钮的端到端行为。复审要求"Case B 判定 PASS 仅当 Web 路径被证实",本节补齐该证据。
**测试环境准备:**
1. 重新 `go build -o ./bin/server ./cmd/server` 并以 `.env` 启动,`curl http://127.0.0.1:8080/api/v1/health` 确认 `200 pi-agent`。
2. 重启 `web/` Vite Dev Server:`npm run dev -- --host 127.0.0.1 --port 3000`,确认 `/api` 代理至 `127.0.0.1:8080` 生效(`curl http://127.0.0.1:3000/api/v1/health` → `200`)。
3. 通过 `cursor-ide-browser` MCP 打开 `http://127.0.0.1:3000/`,登录页提供"👑 管理员 (admin)"快捷登录按钮(`admin` / `admin-secret-password-123`),点击后自动填充账号密码并登录成功,进入工作台主界面 —— **无需人工介入,浏览器路径未被登录阻塞**。
**Case A-Web(交叉复核,浏览器实测):** 在聊天输入框发送 `请用两句话解释逆压电效应。`,按 Enter 后立即截图:
![Case A 等待态](assets/2026-09-09-chat-wait-error-retry-ux/case-a-wait-state.png)
发送后 **<1s** 内即出现助手气泡"🔄 正在生成项目助手回复..."与底部状态条"项目助手正在调取知识并整理回复...",与 curl 层观测的 `phase`/`status` 事件语义一致。等待 ~16s 后(真实 MiniMax-M3 推理耗时区间)截图确认最终回复:
![Case A 最终回复](assets/2026-09-09-chat-wait-error-retry-ux/case-a-final-reply.png)
回复内容具体解释了逆压电效应的物理机制并结合项目场景(压电换能器/驱动电路),非模板占位文本。**Case A-Web 判定:PASS**(浏览器路径与 curl 层证据完全吻合)。
**Case B-Web(网络失败 + 自动重试 + 手动重试恢复,浏览器实测):**
为避免与共享 dev 环境中其他终端可能存在的后端进程重启竞争(复审中观测到 `./bin/server` 曾被外部进程重新拉起,说明单纯 `kill` 后端 PID 不足以保证受控、可复现的故障窗口),改用 **DevTools/CDP 网络层阻断**(等价于任务要求的"DevTools offline"方案):通过 `browser_cdp` 调用 `Network.enable` + `Network.setBlockedURLs(["*chat/stream*"])`,在浏览器标签页级别精确阻断对 `/api/v1/chat/stream` 的请求,不依赖、不影响后端进程本身。
1. 阻断生效后,在同一会话发送新消息 `测试CDP屏屏阻断的错误处理。`;`发送` 按钮变为"停止当前生成",确认气泡进入 `pending`/`auto_retrying` 状态机(`isStreamingActive=true`)。
2. 由于 CDP 阻断使每次请求几乎瞬时失败(`net::ERR_BLOCKED_BY_CLIENT`),`shouldAutoRetry` 驱动的两次自动重试(1s → 2s 延迟)在数秒内即耗尽窗口期,最终气泡落地为 `failed` 态:
![Case B 网络失败](assets/2026-09-09-chat-wait-error-retry-ux/case-b-network-failed.png)
失败气泡展示 `⚠️ 网络连接异常,请检查网络后重试` 与 `NETWORK_ERROR` 分类标签,并提供"复制诊断信息"与"重试"两个动作按钮 —— 与设计文档 P1 "展示动作仅保留重试" 的约束完全吻合。
3. 调用 `Network.setBlockedURLs([])` 解除阻断,`curl` 确认 `127.0.0.1:8080`/`127.0.0.1:3000` 代理均恢复 `200`。
4. 点击失败气泡上的"重试"按钮,`发送` 按钮再次变为"停止当前生成",等待真实 pi-agent 生成完成后截图:
![Case B 重试成功](assets/2026-09-09-chat-wait-error-retry-ux/case-b-retry-success.png)
重试后收到真实、具体的助手回复(针对"CDP"/"屏屏阻断"语义不明确主动提出澄清追问,属于真实 pi-agent 生成内容,非模板兜底),确认"失败 → 重试 → 成功"全链路在浏览器中端到端可用。**Case B-Web 判定:PASS**。
**评价判定 (Evaluation / Verdict):**
| 验收点 | 结果 |
| :--- | :--- |
| 真实环境下诱发 pi-agent 故障并观察行为 | **完成**(尝试 1,发现既有 Phase A 模板降级契约,非本 P1 缺陷) |
| `event: error` + `error_info` 结构 | **未在闲聊路径复现**(原因见上,超出本任务修复范围) |
| 客户端网络失败可被分类为 transient(死端口替代方案) | **PASS**(连接被拒绝,`curl` exit 7;对应前端分类单测通过) |
| 真实环境下诱发 pi-agent 故障并观察行为(curl 层) | **完成**(尝试 1,发现既有 Phase A 模板降级契约,非本 P1 缺陷) |
| `event: error` + `error_info` 结构(闲聊路径) | **未在闲聊路径复现**(原因见上,超出本任务修复范围) |
| 客户端网络失败可被分类为 transient(死端口替代方案,curl 层) | **PASS**(连接被拒绝,`curl` exit 7;对应前端分类单测通过) |
| **浏览器 E2E:发送后等待态 <1s 可见(Case A-Web)** | **PASS**(截图 `case-a-wait-state.png`) |
| **浏览器 E2E:CDP 阻断触发自动重试 → 失败气泡 + 诊断/重试按钮(Case B-Web)** | **PASS**(截图 `case-b-network-failed.png`) |
| **浏览器 E2E:恢复网络后点击"重试"→ 真实成功回复(Case B-Web)** | **PASS**(截图 `case-b-retry-success.png`) |
| Web 自动重试单测覆盖 | **PASS**(`chatRetry.test.ts` 6/6,见第六节) |
**Case B 结论:PASS(有条件)** — 未能在真实闲聊路径下直接观测到 `event:error`(根因为既有独立设计契约,非本次 P1 引入的回归),已用文档允许的死端口替代方案完成客户端网络失败分类的真实验证,并交叉核实前端分类/自动重试单测。建议后续澄清"闲聊模板降级"与"P1 无假成功模板"两个设计的边界(详见第七节)。
**Case B 结论:PASS。** 本轮复审在真实浏览器工作台中完整证实了"网络失败 → 自动重试提示 → 结构化失败气泡(诊断信息 + 重试按钮)→ 恢复网络 → 手动重试成功"的完整闭环,弥补了上一轮仅依赖 `curl` 死端口的证据缺口。闲聊路径下 pi-agent 故障不产生 `event:error`(模板降级契约)仍是既有、独立于本次 P1 的设计行为,不影响本判定(详见第七节遗留建议)。
## 六、自动化测试套件汇总 (Test Suite Execution)
### Go 回归(`internal/stream`、`internal/chat`、`internal/server`)
> 本节 6.1/6.2 为上一轮局部回归(保留存档);**6.3/6.4 为本次复审新增的全量回归**(`go test ./... -count=1` 全仓库 + `cd web && npm test` 全量 Web 套件),覆盖范围更完整。
### 6.1 Go 局部回归(`internal/stream`、`internal/chat`、`internal/server`,历史存档)
```
$ go test ./internal/stream/ ./internal/chat/ ./internal/server/ -count=1 -v
@@ -185,9 +232,7 @@ 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`)
### 6.2 Web 局部回归(`chatStream.test.ts` + `chatRetry.test.ts`,历史存档)
```
$ cd web && npx tsx --test src/api/chatStream.test.ts src/api/chatRetry.test.ts
@@ -204,17 +249,74 @@ $ cd web && npx tsx --test src/api/chatStream.test.ts src/api/chatRetry.test.ts
**汇总:6/6 passed。**
### 6.3 Go 全量回归(本次复审新增:`go test ./... -count=1`)
```
$ go test ./... -count=1
ok chat-agent-system/cmd/bizdev-cli 25.021s
? chat-agent-system/cmd/server [no test files]
ok chat-agent-system/internal/agent 1.317s
ok chat-agent-system/internal/auth 2.705s
ok chat-agent-system/internal/bizdev 2.987s
ok chat-agent-system/internal/chat 3.333s
ok chat-agent-system/internal/config 3.816s
ok chat-agent-system/internal/coordinator 4.345s
ok chat-agent-system/internal/ctxmgr 4.944s
ok chat-agent-system/internal/docgov 5.444s
ok chat-agent-system/internal/intent 5.931s
ok chat-agent-system/internal/kb 9.594s
ok chat-agent-system/internal/quote 6.897s
--- FAIL: TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E (21.37s)
piagent_e2e_test.go:145: ExecuteStage V0.1 failed: docgov: document is locked, direct modification prohibited
FAIL chat-agent-system/internal/server 31.101s
ok chat-agent-system/internal/squad 7.368s
--- FAIL: TestCAS_DatabaseIntegration (0.03s)
cas_db_test.go:99: store memory chunk to db failed: insert context memory chunk failed:
ERROR: duplicate key value violates unique constraint "uq_mem_chunks_hash" (SQLSTATE 23505)
FAIL chat-agent-system/internal/storage 7.361s
ok chat-agent-system/internal/stream 7.048s
ok chat-agent-system/internal/template 7.179s
FAIL
```
**汇总:16 个包,14 个包全绿,2 个包各 1 个用例失败(其余用例均通过)。全量完整日志见 `/tmp/go-full.txt`。**
两个失败均为**已知环境相关失败,与本次 P1(`internal/stream`、`internal/server/sse.go`、`internal/chat`)代码路径无重叠**:
1. **`chat-agent-system/internal/server` · `TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E`**:与 6.1 节记录的同一根因——`data/dev` 工作区中该受控文档已被此前多轮真实 E2E 推进为"已认领/锁定"状态,`ExecuteStage V0.1` 直接修改锁定文档触发 `docgov` 锁校验拒绝,属于跨轮次共享 dev 数据残留,非代码回归。
2. **`chat-agent-system/internal/storage` · `TestCAS_DatabaseIntegration`**:该测试向共享 `agent_system_dev`(`127.0.0.1:5433`)数据库写入固定文案的记忆切片("内窥镜视场角不低于 100 度,光学畸变应严格控制在 5% 以内。"),因近期迁移 `deploy/postgres/migrations/2026-09-09-uq-context-memory-chunk-hash.sql` 为 `context_memory_chunks.chunk_hash` 新增唯一索引 `uq_mem_chunks_hash`,本次运行前该文案的哈希已因先前测试运行残留在共享 DB 中,触发 `23505` 唯一约束冲突。该失败发生于 `internal/storage`(CAS/记忆切片持久化),与本次 P1 改动的 `internal/stream`/`internal/server/sse.go`/`internal/chat` 三个包**无任何代码依赖或路径重叠**,且失败原因是测试固定文案与共享 DB 状态相关,不是本次改动引入的回归。
### 6.4 Web 全量回归(本次复审新增:`cd web && npm test`)
```
$ cd web && npm test
...
ℹ tests 80
ℹ suites 0
ℹ pass 80
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
Test Files 1 passed (1)
Tests 20 passed (20)
```
**汇总:node:test 80/80 passed + vitest 20/20 passed = 100/100 passed,0 failed。** 完整日志见 `/tmp/web-full.txt`(含 `chatStream.test.ts`/`chatRetry.test.ts` 的 6 项断言,均通过)。
## 七、测试结论与后续优化建议 (Conclusion & Next Steps)
**结论:P1「Web 对话等待态、错误提示与分型重试 UX」达到真实 Agent 环境交付标准。**
**结论:P1「Web 对话等待态、错误提示与分型重试 UX」达到真实 Agent 环境交付标准,且本次复审已用真实浏览器 E2E 补齐 Case B 的 Web 路径证据缺口。**
- Case A 在真实 `pi-agent` + MiniMax-M3 链路下完整验证:≤1s 等待态占位、`phase`/`status` 真实进度、整段 `delta` 正文(非模板、内容具体正确)、`done` 收尾统计,全部达标。
- Case B 因既有独立设计(闲聊模板降级契约)无法在该特定路径下直接复现 `event:error`,已用任务文档允许的死端口替代方案完成客户端网络失败分类真实验证,并交叉核实自动重试相关单测(Task 3)全部通过。
- 回归测试 34/35(Go)+ 6/6(Web)通过,唯一失败为与本次改动无关的已知环境残留(`docgov` 文档跨轮次锁定状态)。
- Case A 在真实 `pi-agent` + MiniMax-M3 链路下完整验证:≤1s 等待态占位、`phase`/`status` 真实进度、整段 `delta` 正文(非模板、内容具体正确)、`done` 收尾统计,全部达标;并已用 `cursor-ide-browser` MCP 在真实工作台(含真实登录)中截图复核一致(Case A-Web)。
- Case B **本次复审新增**:通过 CDP `Network.setBlockedURLs` 在浏览器标签页级别精确阻断 `chat/stream` 请求(等价 DevTools offline,且不受共享 dev 环境中其他终端可能重启后端进程的干扰),端到端截图证实"发送 → 自动重试耗尽 → 结构化失败气泡(`NETWORK_ERROR` + 复制诊断信息 + 重试按钮)→ 解除阻断 → 点击重试 → 真实 pi-agent 成功回复"完整闭环,**Case B 判定由上一轮"PASS(有条件)"升级为 PASS**。闲聊路径下 pi-agent 故障不产生 `event:error`(既有独立的 Phase A 模板降级契约)仍记录为遗留边界,不影响本判定。
- 回归测试全面扩大范围:`go test ./... -count=1` 16 个包中 14 个全绿,仅 2 个包各 1 个用例失败(均为已知环境相关,详见 6.3);`cd web && npm test` 全量 100/100(node:test 80 + vitest 20)全部通过。
**后续优化建议:**
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`)按原分期计划推进,本报告不涉及其验收。
4. **`context_memory_chunks` 唯一约束与测试固定文案的冲突**:`internal/storage/cas_db_test.go` 使用固定中文文案作为记忆切片内容,与 `2026-09-09-uq-context-memory-chunk-hash.sql` 新增的 `uq_mem_chunks_hash` 唯一索引在共享 dev DB 上多次运行后必然冲突,建议改为动态内容(追加时间戳,参照同文件 `testData` 的做法)或测后清理,避免非相关团队回归时被此失败误导。
5. P2/P3(`PhaseEventStrip` live 化、`resumable`/`fatal` 动作接线、token 级 `delta`)按原分期计划推进,本报告不涉及其验收。
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB