重审 37 条 chat 路径的测试设计:11 个人肉验收缺陷无一被抓住,成因归类 + 计划修订
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 0
- Avg merge
- 1h 7m
- Merged PRs (30d)
- 969
Description
## 背景
人类 2026-09-09/10 在 devapp 人肉验收,报了 11 个真实缺陷(#3186 #3204 #3207 #3211 #3212 #3214 #3243 #3244 #3251 #3252 #3221)。**37 条路径全绿也拦不住其中任何一个。** 本 issue 是「重审 37 条测试设计本身」的产出,范围是**设计缺口**,不是跑测试。
实测 SHA `f08cbe1ea`(`git rev-parse origin/main`)。
---
## 问 1:11 个缺陷逐个对照 37 条,成因归类
| 缺陷 | 本该由哪条抓住 | 为什么没抓住 | 成因分类 |
| --- | --- | --- | --- |
| #3186 审批弹窗点了没反应 / #3212 授权不被记住 / #3221 授权键 ≠ 分级键 | B4 四选一审批 | B4 判的是**单次裁决**的服务端语义。`copilotkit-v2-hitl.spec.ts:102` 循环四个决策按钮,每个只 `toBeVisible()`——**按钮的作用域语义一个字都没读**。「裁决之后同一 run 内会不会再问」是跨调用的记忆,判据里没有这一段 | **路径存在但判据太弱** |
| #3207 该弹的窗口不弹 / #3244 提交后又弹一次 | B4 / B5 | B4/B5 的用例**先决条件**就是「弹窗已经出现」,从这个前提往下测。「该弹而没弹」在它们眼里是前置失败,红落在 `waitFor` 超时上,与环境问题分不开。同形证据:`chat-agent-skill-context.spec.ts:210` 把「面板可见」放宽成 `toBeAttached()`(注释 197-202 自陈),**零尺寸面板通过**——正是「存在但用户看不到」 | **被测的那件事被写成了前置条件** |
| #3204 失败的工具卡显绿对勾 | D1 / D2 | D1/D2 判「卡片走到终态」「折叠/展开/回放」。同一次调用在外层折叠行与内层卡片上**各渲染一次状态**,原表没有任何一条断言「两处必须恒等」 | **根本没有对应路径(是不变量,不是功能)** |
| #3243 / #3230 刷新后 10 个画布全消失 | C4(一轮两画布)、C5(连续多轮) | #3243 的失效要三件事同时成立:数量大(10)、**刷新发生在生成中途**、刷新后继续生成。C4/C5 的量级是 2 和 3,且刷新只在生成完成后做一次——三件一件都不成立 | **路径存在但构造覆盖不到这个区间** |
| #3214 空白会话也显示阶段条 / #3208 卡在「准备」8 分钟 / #3251 步骤总数 10↔9 跳变 | 无(B7 沾边,但在手动记分牌车道) | 唯一在阻塞车道碰过 plan panel 的是 `agent-workbench-ui-refinement.spec.ts:42`:`page.route(/\/plan-control\/threads\/[^/]+\/ledger/)` 把账本端点**整段 fulfill 成手写 JSON**,文件注释自陈「only ledger HTTP responses are controlled … not backend checkpoint execution」。`phase:"preparing"` + `steps:[]` 是**夹具里写死的输入**,派生逻辑一行都没执行 | **路径不存在,且现有用例把被测逻辑 stub 掉了** |
| #3211 整轮失败零产出、原因不可分辨 | F1 错误横幅、F7 上游断流 | 两条都只问「失败有没有被诚实呈现」。`copilotkit-v2-error-banner.spec.ts:106` 与 `chat-path-f7-upstream-stream-abort.spec.ts:69` 断言的是横幅文案**非空**——#3211 里横幅出现了、界面也可用,**F1/F7 会双绿** | **判据的抽象层次不到位** |
| #3252 画布身份判定粒度是模板名 | C1 / C2 / C4 | C4 用「表头带之一/之二」区分两个画布,**画布内容本身从没被比较过**;C2 的「内容相同」判据是两张 PNG 字节数差 < 15%(`chat-diagram-save-reopen-roundtrip.spec.ts:409-412`) | **判据太弱(痕迹型代理)** |
**没有一个缺陷是「37 条跑了但漏判」——全部是设计层面就够不着。**
---
## 问 2:37 条的抽象层次对不对?
不对。37 条几乎全部是**「功能能不能跑通」**,而人类撞到的三类一条系统覆盖都没有:
- **状态一致性(同一事实两处声明)**:0 条。
- **恢复路径(刷新 / 切走再切回 / 生成中途刷新)**:只有 A4、B5、C2 沾边,且都是「完成之后刷一次」,没有一条在**进行中**刷新。
- **失败可诊断性**:0 条。F1/F7 只到「诚实结束」,不到「原因可分辨」。
---
## 问 3:判据是「元素存在」或等价物的,逐条点名
已写进矩阵的「判据强度抽查」一节(见 PR)。要点:
- `chat-task-workbench-workflow-states.spec.ts:119` —— `data-phase` 匹配 `^(preparing|planning|executing|approving|done|failed)$`,**六个全收**。在刚断言过进入 planning 的位置上,**一个卡在 `preparing` 的 run 照样通过**。这就是「缺陷被写进测试当成期望值」的实例,且它正对着 #3208/#3214。
- `chat-diagram-save-reopen-roundtrip.spec.ts:409-412` —— PNG 字节数比(阈值 0.15),已由 #3171 记录。
- `core-journey-04-canvas-template-lifecycle-chat.spec.ts:170-171` —— C3 判据是「该项目 chat 可达」,实际只断言线程列表与「新建」按钮**可见**;整条用例**从没建过一条线程、没发过一条消息**。C3 的 `已覆盖` 名不副实。
- `chat-path-a5-cold-start-first-paint.spec.ts:44,59` —— A5 全部判据 = 输入框 `toBeVisible()` + `readyMs > 0`。
- `copilotkit-v2-voice-input.spec.ts:68` —— `laterValue.length >= midValue.length`,**长度相等(转录停滞)也通过**。
- `chat-vision-honest-degrade.spec.ts:115` —— `toHaveCount(1)`,回复存在即可,「诚实告知」四个字没被断言。
---
## 问 4:哪些条目「从没真正执行过」?统计现状
**这是本轮最刺眼的发现,而它此前在本表里一个字都没写过。** 逐条读 `.github/workflows/harness-verify.yml` 的 job `if:`:
| 车道 | `if:` | 什么时候真跑 | 行数 |
| --- | --- | --- | --- |
| `chat-read` | `github.event_name != 'pull_request' && (… \|\| inputs.run_e2e_full)` (`:565`) | push/schedule。**PR 上一次都不跑** | 26 |
| `chat-path-coverage` | `github.event_name == 'workflow_dispatch' && inputs.run_chat_path_coverage` (`:682`) | **只有人手动 dispatch 并勾开关** | 4(C6/C8/F2/F5) |
| `chat-task-workbench` | `github.event_name == 'workflow_dispatch' && inputs.run_chat_task_workbench` (`:729`) | 同上,且是**设计上会红的记分牌** | 3(B7/D6/F4) |
| `real-model-smoke` | 手动 dispatch;spec 另有 `real-model-pdf-smoke.spec.ts:47` 的运行期 `test.skip` | 缺凭据时整条跳过 | 1(C7) |
| `—` | — | 从不 | 1(F3)+ 新增 6 |
**43 条里只有 26 条挂在会自动执行的车道上,而那 26 条在 PR 上也不跑。**
停放的断言(现存 3 条 `test.fixme`,都写明了阻塞 issue,处置正当):
- `chat-path-c8-subtask-artifact-writeback.spec.ts:162`(C8;**这是该文件唯一的 test,C8 执行覆盖为零**)
- `chat-attachment-preview-download.spec.ts:156`(#3019)
- `chat-agent-skill-context.spec.ts:492`(#3023)
---
## 建议新增的路径(每条都指名它抓今天哪个真实缺陷)
矩阵已加六行(本 PR),全部 `未覆盖`:
| 新行 | 抓的缺陷 |
| --- | --- |
| **B8** 授权记忆与分级同键 | #3212、#3221 |
| **B9** 待确认请求必可见可操作 | #3207、#3244 |
| **C9** 多产物刷新恢复(生成中途刷新 + N=10) | #3243、#3230 |
| **D7** 工具步骤状态单源 | #3204 |
| **F8** 阶段与进度派生自真实账本 | #3214、#3208、#3251 |
| **F9** 失败可诊断性 | #3211 |
**没有提任何抓不住真实缺陷的新增项。**
## 建议改写的判据(实现缺口,不在本 PR 范围)
1. `chat-task-workbench-workflow-states.spec.ts:119` 的六选一正则收窄为该时刻唯一合法的那个 phase。**反证**:把派生改成恒返 `preparing`,这条必须红。
2. F1/F7 的「横幅非空」升级为「横幅含**可分辨的失败类别**」(枚举,不是自由文本)。反证:把所有失败原因替换成同一句通用措辞,必须红。
3. C3 的 chat 半段补上「真的建一条线程、发一条消息、模板可达」,否则把 C3 降为 `部分`。
4. C2 的 PNG 字节比换成语义读(画布对象的权威读),见 #3171。
## 建议新增的机械门(实现缺口)
- `lint-chat-path-coverage.mjs` 加第 ⑦ 条:标 `已覆盖` 的行,其车道在 `harness-verify.yml` 里的 job `if:` 不得只由 `workflow_dispatch` + 开关满足。**反证**:把任意 `已覆盖` 行的车道改成 `chat-path-coverage`,门必须红。
## 一类结构性判据(对应「同一事实两处各算一遍」今天出现的第七次)
七次现象各不相同(#3190 轨迹锚点 / #3204 工具卡 / #3214 阶段派生 / #3244 弹窗挂载 / #3221 授权键 / #3252 画布身份 / 流式与落库本轮消息),修法全部是收敛为单一来源。建议在 `apps/web/e2e/support/` 加一个跨路径复用的不变量断言 `assertSingleSourceOfTruth`(探针取自 DOM 的 `data-semantic-*`、**零探针必须判红**、必须造反证),细节写在矩阵新增一节里。它需要产品侧先把渲染点标出来,是独立的一件活。
## 范围声明
- **设计缺口**(本 issue 范围):抽象层次、判据强度、执行现实、新增路径。
- **实现缺口**(不在本 PR):改写判据、加第 ⑦ 条门、`assertSingleSourceOfTruth` 的落地。
本 PR 只改 `.harness/instructions/chat-path-coverage-matrix.md` 一个文件。
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with .harness/instructions/chat-path-coverage-matrix.md, then inspect the cited specs and the chat job conditions in .github/workflows/harness-verify.yml. Compare each reported defect with its path, assertion strength, and execution lane. Done means the matrix records the design and execution gaps and the six proposed paths without implementing the listed test changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github-actions, typescript
- Domain
- ci-cd, testing
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100