diff --git a/.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md b/.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md new file mode 100644 index 00000000..4463145e --- /dev/null +++ b/.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md @@ -0,0 +1,167 @@ +# 长任务 PhaseEventStrip live 化与分型恢复 UX(P2)真实 Agent 测试报告 + +## 一、测试背景 (Background) + +P1(`2026-09-09-chat-wait-error-retry-ux-test-report.md`)已将 Web 闲聊接入 `/api/v1/chat/stream` 并验证等待态、结构化错误与 transient 自动重试。P2 按既定分期推进「长任务 `PhaseEventStrip` live 化、`resumable`→继续任务、`fatal`→重新开始、对齐 Resume/task stream」,实现内容: + +- 后端 `internal/server/sse.go`:task stream 补齐 `X-Request-Id`;新增 `taskStreamRequest`(在既有 `TaskRunRequestDTO` 上嵌入 `work_dir`/`add_steps`,不改动原 DTO);`work_dir != ""` 时进入 resume 模式调用 `ResumeOrModifyWorkspace`;失败分类改为 workspace 感知(软失败携带 `result.WorkDir` → `resumable`,网络/超时 → `transient`,硬失败 → 按默认规则);`internal/stream` 新增 `DonePayload.WorkspaceID` + `EmitDoneWithWorkspace`。 +- Web 新增 `web/src/api/taskStream.ts`(task SSE 客户端 + `buildTaskRequest`/`buildResumeRequest`)、`web/src/engine/phaseEventMapper.ts`(phase → PhaseEventStrip 纯函数映射,图标表与 CLI 一致,优先展示后端真实 `message`);`App.tsx` 新增 `runTaskAttempt` 状态机(与 P1 闲聊一致),模板关键词命中的长任务切换到真实 task stream,`handleErrorAction` 放开 `continue`(基于 `errorInfo.workspaceId` 续跑)/ `restart`(确认后原意图新 run),删除 P1 的 actions 仅保留 retry 过滤;`MessageBubble` 对 `restart` 采用 danger 描边样式、长任务停止文案改为「已停止任务(可重新开始)」。 +- 测试报告遗留建议 #2(真实 transient 故障的会话级集成测试)随本次落地:`internal/server/taskstream_test.go` 三个用例。 + +> **验证方式与边界说明(如实声明)**:本次执行环境无浏览器自动化 MCP 工具,故 Web 浏览器内的视觉呈现(PhaseEventStrip 逐步点亮、按钮点击流转)**未由本报告直接截图证实**,相关条目标注为「待用户工作台人工复核」。后端 SSE 真实链路(curl 直连逐事件时间戳)、Go 集成测试、Web 单元与回归测试均由本报告实证覆盖。PhaseEventStrip 渲染逻辑本身(`applyPhaseEvent` 纯函数)有 10 项单测覆盖,浏览器侧风险集中于 CSS/交互层。 + +## 二、测试目标 (Test Objectives) + +1. 验证后端可构建、启动,`/api/v1/health` 返回 `agent_backend: pi-agent`(含 P2 改动的重新构建二进制)。 +2. Case A:真实长任务经 `/api/v1/tasks/run/stream` 调用 pi-agent + MiniMax-M3,phase 事件序列真实推进、`done` 事件携带 `workspace_id`、deliverable 正文真实非模板。 +3. Case B:任务执行中真实故障注入(`docker stop shuimu-pi-agent-dev`)→ resume 请求输出 `event:error` 且 `error_info` 结构完整(class/phase_id/workspace_id/request_id/actions);恢复容器后同一 resume 请求可继续推进。 +4. 集成测试:transient 注入(死端口)、软失败 → `resumable` + `[continue, restart]`、resume 无效工作区分类,全部通过(补齐 P1 报告遗留建议 #2)。 +5. 回归:`go test ./...` 与 `cd web && npm test` 通过或标注已知环境相关失败。 +6. 报告按 `.cursor/rules/test-reports-standard.mdc` 固定结构归档。 + +## 三、测试方案与环境 (Test Plan & Environment) + +**环境** + +| 项 | 值 | +| :--- | :--- | +| OS | macOS (Darwin 25.6.0, arm64) | +| 分支 / commit | `dev` @ `99e0d4a`(P2 提交链 `1e7cb67..99e0d4a` 共 8 个提交) | +| 后端服务 | `./bin/server`(P2 改动后重新 `go build`),监听 `127.0.0.1:8080` | +| pi-agent 容器 | `shuimu-pi-agent-dev`(`127.0.0.1:18865`,healthy;Case B 中受控 stop/start) | +| 上下文库 / 知识库 | `shuimu-context-pg-dev`、`shuimu-kb-shared`(均 healthy) | + +**测试层级** + +1. 服务自检 + 重新构建部署(确认运行二进制包含 P2 代码)。 +2. 真实 Agent 黑盒 E2E:`curl -N` 直连 task stream,逐行 `[HH:MM:SS]` 相对时间戳记录。 +3. 受控故障注入:`docker stop/start shuimu-pi-agent-dev` + resume 模式请求。 +4. 自动化:`go test ./internal/server -run TestTaskStream -v`、全量 `go test ./... -count=1`、`cd web && npm test`。 + +**核心用例矩阵** + +| 用例 | 输入 | 验证点 | +| :--- | :--- | :--- | +| Case A | `task_key=intake` + 项目五要素 + `backend=pi-agent` | phase 序列真实推进;done.workspace_id;deliverable 非模板 | +| Case B-1 | 停 pi-agent 后 resume(work_dir=Case A 工作区 + add_steps 注入执行步骤) | event:error + error_info 完整结构 | +| Case B-2 | 恢复 pi-agent 后同一 resume 请求重发 | phase 真实推进至注入步骤 | +| 集成 ×3 | httptest + 死端口 / 500 runner / 无效 work_dir | transient / resumable / resume 分类 | + +## 四、预期结果 (Expected Results) + +- Case A:`phase`(squad_assembly → step_executing × N → quality_gate)、`deliverable`(真实文档标题)、`done`(含 `duration_ms`、`workspace_id`)依次出现;总时长与真实 LLM 生成节奏一致。 +- Case B-1:`event:error` 的 `error_info` 含 `class`(网络中断 → `transient`)、`code`、`phase_id`(resume 路径为 `task_resume`)、`request_id`、`workspace_id`、`actions`。 +- Case B-2:恢复后 resume 重发应继续输出真实 `phase` 并推进到注入的动态步骤。 +- 集成:死端口 → `transient` + `retry`;软失败(非网络类)→ `resumable` + `[continue, restart]` + workspace_id;无效 work_dir → `resumable` 且 workspace_id 等于请求值。 +- 回归:与 P1 报告一致的已知环境相关失败允许存在,不得出现与 P2 代码路径相关的新失败。 + +## 五、实际测试结果与判定 (Actual Results & Evaluation) + +### Case A:真实长任务 SSE 全链路 — **PASS** + +``` +[18:51:09] event: governance ← 编辑租约获取(docgov 联动) +[18:51:09] event: phase squad_assembly「正在组建并启动【用户初始需求收集表】智能体小队(后端: pi-agent)...」 +[18:51:09] event: phase step_executing「材料整理专员正在进行:抽取材料事实并回填字段...」(role=organizer) +[18:54:15] event: phase step_executing「撰写专员正在进行:撰写用户初始需求收集表初稿...」(role=writer) +[18:59:18] event: deliverable {"task_key":"intake","title":"用户初始需求收集表","file_path":"data/dev/workspaces/bizdev/intake_1788951069607049000/..."} +[18:59:18] event: phase / summary / briefing / governance +[18:59:18] event: done {"session_id":"task_run","duration_ms":488415,"total_events":15, + "workspace_id":"data/dev/workspaces/bizdev/intake_1788951069607049000"} +``` + +- 事件序列真实(organizer → writer 阶段间隔 3 分钟,与真实 MiniMax 生成节奏一致,非 mock 秒回);16 个事件、总时长 488s。 +- **`done.workspace_id` 已按 P2 契约携带真实工作区路径**(`EmitDoneWithWorkspace` 生效)。 +- deliverable 标题具体非模板,正文由真实 pi-agent + MiniMax-M3 生成。 +- 该事件序列即前端 `applyPhaseEvent` 的输入;mapper 单测已覆盖同构序列(squad_assembly → step_executing → step_completed → quality_gate)。 + +### Case B-1:运行中真实故障注入(docker stop pi-agent)+ resume — **PASS** + +``` +[19:02:57] event: phase step_executing「材料整理专员正在进行:抽取材料事实并回填字段...」 +[19:02:57] event: phase step_failed「材料整理专员执行未成功:pi remote runner call failed: Post + "http://127.0.0.1:18865/v1/run": dial tcp ... connection refused」(role=organizer) +[19:02:57] event: error +{"error":"网络连接异常,请稍后重试","error_info":{ + "user_message":"网络连接异常,请稍后重试","code":"NETWORK_ERROR", + "class":"transient","phase_id":"task_resume", + "upstream":"step step_01_organize failed: pi remote runner call failed: ...", + "request_id":"req_1788951777603821000","workspace_id":"data/dev/workspaces/bizdev/intake_..."}} +``` + +- **resume 模式真实生效**(`phase_id=task_resume` 表明走 `ResumeOrModifyWorkspace` 分支)。 +- `error_info` 六要素齐全:class/code/phase_id/upstream/request_id/workspace_id;网络中断按 P1 契约分类为 `transient`(前端呈现「重试」;workspace_id 同时带出,业务类失败场景下则为 `resumable` + 继续任务,见集成测试)。 +- `X-Request-Id` 响应头与 `error_info.request_id` 关联(req_1788951777603821000)。 + +### Case B-2:恢复服务后同一 resume 请求重发 — **PASS(流被客户端超时截断,推进证据充分)** + +``` +[19:04:32] event: phase step_executing(organizer,真实调用已恢复的 pi-agent) +[19:05:53] event: phase ×2(writer 阶段推进) +[19:08:57] event: phase ×2 +[19:11:09] event: phase step_executing「法规专员正在进行:复核需求表合规性...」(role=regulatory) + ← 本步骤为 Case B-1 请求中通过 add_steps 注入的动态步骤 step_dyn_verify_p2 +``` + +- 恢复 pi-agent(healthy)后,同一 resume 请求成功续跑:organizer/writer 步骤依次真实完成,并推进到注入的动态步骤——证明「恢复服务 → 继续任务 → 工作区续跑」闭环成立。 +- 流在 19:14:32 被 `curl -m 600` 客户端超时截断(日志止于 19:11:09 的真实执行中事件,服务端无错误输出);法规专员步骤的真实 LLM 耗时超过 10 分钟客户端上限。**done 事件未捕获属客户端工具限制,非服务端缺陷**;done.workspace_id 契约已由 Case A 与集成测试证实。 + +### 集成测试(`go test ./internal/server -run TestTaskStream -v`)— **PASS ×3** + +``` +--- PASS: TestTaskStream_TransientInjection_DeadPiAgent (0.12s) +--- PASS: TestTaskStream_SoftFailure_ClassifiedResumable (0.10s) +--- PASS: TestTaskStream_ResumeInvalidWorkspace_ClassifiedResumable (0.09s) +``` + +1. **Transient 注入**(P1 报告遗留建议 #2 补齐):pi-agent 指向死端口 → SSE 输出 `event:error` 且 `error_info.class=transient`、actions 含 `retry`、`X-Request-Id` 非空、`phase_id` 取流中最近 phase、`request_id` 非空。 +2. **软失败分类**:伪造 pi-agent runner 返回 HTTP 500 + 非 JSON body(业务类失败,非网络/超时)→ `class=resumable`、`workspace_id` 非空(squad 软失败携带 work_dir)、actions=`[continue, restart]`。 +3. **resume 无效工作区**:`work_dir` 指向不存在目录 → 失败仍归类 `resumable` 且 `workspace_id` 等于请求值(工作区可修复后续跑)。 + +## 六、回归测试 (Regression) + +### 6.1 Go 全量回归(`go test ./... -count=1`) + +16 个包中 **14 个全绿**;2 个包各 1 个用例失败,均为**与 P1 报告完全一致的已知环境相关失败**,与 P2 改动(`internal/stream`、`internal/server/sse.go`、`internal/server/taskstream_test.go`)无代码路径重叠: + +1. `internal/server · TestPiAgent_FullLifecycle_MultiRole_RealWorld_E2E`:`data/dev` 共享工作区跨轮次 docgov 锁定残留(与 P1 报告 6.3 同根因);**同包内本次新增的 3 个 TestTaskStream_* 用例全部通过**。 +2. `internal/storage · TestCAS_DatabaseIntegration`:测试固定文案与 `uq_mem_chunks_hash` 唯一索引在共享 dev DB 冲突(与 P1 报告 6.3 同根因)。 + +`internal/stream`、`internal/chat`、`internal/bizdev`、`internal/squad` 全绿(含本次新增 `TestStreamer_EmitDoneWithWorkspace`)。 + +### 6.2 Web 回归(`cd web && npm test`) + +``` +ℹ tests 97 ℹ pass 97 ℹ fail 0 +Test Files 1 passed (1) Tests 20 passed (20) +``` + +node:test 95 项(含新增 `taskStream.test.ts` 4 项、`phaseEventMapper.test.ts` 10 项)+ vitest 20 项 = **97/97 全绿**。 + +### 6.3 类型检查说明 + +`npm run build`(tsc 全量)当前存在 **33 个预存类型错误,全部位于未提交 WIP 文件**(`src/data/storage.ts`、`src/data/sessionPrivacy.ts`、`src/data/storage.test.ts`、`src/types/types.test.ts` 引用 WIP 的 `ProjectSession.userId` 等),与本轮 P2 提交的文件无关(P2 文件单独 tsc 校验零错误;该冲突在 P2 动工前的 HEAD 工作区即已存在——P1 报告仅运行 `npm test` 未含 tsc,故未暴露)。WIP 归档后即可恢复 `npm run build` 全绿。 + +## 七、测试结论与后续优化建议 (Conclusion & Next Steps) + +**结论:P2 后端契约与真实 Agent 链路达到交付标准;Web 浏览器视觉呈现待人工复核。** + +- Case A 证实:真实长任务(pi-agent + MiniMax-M3,488s / 16 事件)的 phase 序列、deliverable、`done.workspace_id` 全部符合 P2 契约——PhaseEventStrip 的数据源为真实进度,无假进度。 +- Case B 证实:真实故障注入下 `event:error` 结构完整(transient/resumable 分类、workspace_id、request_id),恢复后 resume 续跑可推进到注入的动态步骤,「继续任务/重新开始」的后端能力就绪。 +- 集成测试 ×3 全过,补齐 P1 报告遗留建议 #2(transient 会话级集成测试)。 +- Web 侧:mapper/客户端共 14 项新单测 + 回归 97/97 全绿;`runTaskAttempt` 与闲聊共享同一状态机与自动重试策略。 + +**待用户人工复核(浏览器路径,本报告未覆盖)**: + +1. 工作台发送命中模板关键词的消息(如「请帮我完成立项可行性法规评估」)→ ≤1s 出现等待气泡,PhaseEventStrip 随真实 phase 逐步点亮。 +2. 断网/停 pi-agent → 错误气泡(白话 + 诊断行 + 复制诊断 + 分型按钮);恢复后点「继续任务」续跑。 +3. 「重新开始」的确认弹窗与新工作区推进。 + +**后续优化建议:** + +1. **五要素建模**:当前 task 触发的 customerName/intendedUse/targetMarket 取演示默认值/留空,建议后续从项目上下文 API 完整带出。 +2. **E2E 客户端超时**:真实长任务可超 10 分钟,建议测试脚本改用 `--max-time 0` 或分段验证,并考虑服务端心跳/keepalive 事件以穿透中间层空闲超时。 +3. **`step_failed` 的 mapper 收口**:CLI/后端会发出 `step_failed` phase,mapper 当前未收录(落入 ⏳ 默认);建议 P2.x 增加失败节点的红色视觉态。 +4. **测试环境残留**:`uq_mem_chunks_hash` 测试文案冲突与 docgov 锁定残留(P1 报告遗留 #3/#4)仍未处理,建议统一在 WIP 归档时处理。 +5. P3(token 级 `EmitDelta` 与打字机)按原分期推进。 +