boardx / boardx/workspacex

fix(chat-read): 组 3 定性 —— A组6条=#2948 把常显审批改成模态卡(真回归);B组=#2927 删掉 write_todos 卡片;C组=#2927 自带新 spec 合入即红;D组=#2869 让 chat_message_id 映射退化

Open
#2,999 25 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 7m
Merged PRs (30d)
969

Description

基线 SHA `938d16bf3a49c4047f42bbc697833cd921d1053e`,e2e-full run
[34155551896](https://github.com/boardx/workspacex/actions/runs/34155551896) job `101851557624`。
本 issue 只覆盖 #2996 切出的**组 3**(在 #2890 合入点 `d30ac48e8` 还绿、到基线才红的那批)。
组 1 = #2997(rewrite),组 2(v2 正文恒空)另开。

## 先纠正一条基线结论

#2996 正文写「约 36 条首错是『没跑到』不是『不符合契约』」。组 3 这 16 条**不成立**:
其中 12 条的首错是这条测试自己的契约断言(`toHaveLength` / `toBe` / 指名 testid 从未 attach),
不是超时或元素未出现。逐条首错见下。只有 A 组里 3 条是 `Test timeout`,且它们的 call log
明确写着在等一个**已经不存在的按钮**,同样不是环境。

---

## A 组(6 条)—— #2948 把审批卡的四个按钮换成了模态 ToolPermissionCard

引入点:**`ea8de6ea9` / PR #2955(实现 issue #2948)**,`apps/web/components/chat/workbench/restored-run-approval.tsx`。

改动前 `` 里直接摊四个 ``:
`仅本次允许 / 本任务内允许 / 以后都允许 / 拒绝`。
改动后 section 里只剩一个「打开工具审批」按钮 + 一个 **Radix `Dialog`**,
裁决按钮搬进 `ToolPermissionCard`。两处硬断链:

1. **标签变了**:`tool-permission-card.tsx:118` 是「本 **run** 内都允许」,不是「本**任务**内允许」。
2. **DOM 位置变了**:`DialogContent` 走 portal 渲染到 `document.body`,
所以 `getByTestId('restored-run-approval').getByRole('button', …)` 的子树里
**永远**找不到裁决按钮——与运行时间、负载无关。

受害测试与首错:
- `copilotkit-v2-hitl.spec.ts:60` → `restored-run-approval` 内找不到「仅本次允许」
- `copilotkit-v2-hitl.spec.ts:70 / 76 / 89` → 同一个 `decide()` helper 卡在
`getByTestId('restored-run-approval').getByRole('button', {name:'拒绝'|'仅本次允许'|'以后都允许'})`
- `agent-workbench-control-acceptance.spec.ts:82` → 卡片 `toBeVisible` **通过了**(第 92 行),
第 94 行点按钮超时。这条最能说明问题:不是「没跑到」,是卡片在、按钮没了。
- `copilotkit-v2-hitl-dialog-dismiss.spec.ts:8` → 卡片 60s 未出现。

**判定 (a) 产品回归,不是测试过期**——`copilotkit-v2-hitl-dialog-dismiss.spec.ts` 存在的唯一目的
就是钉死「常显审批**无全屏遮罩**」这条已验收的不变量(`OVERLAY_SELECTOR` 断言 count 0)。
`components/ui/dialog.tsx:47` 的 `DialogContent` **无条件渲染 ``**,
而 #2948 的 Dialog 默认 `open`、没有 `modal={false}`。同一批里 `91462d788`(PR #2957 / issue #2950)
又把 `interrupt-decision-dialog.tsx` 从 `modal={false}` 改成了模态。
两次改动一起把「审批常显、不遮挡、Escape 不裁决」这条锁过的 UX 退回成了全屏模态。

#2948 更新了 `chat-task-workbench-approval.spec.ts` / `-workflow-states.spec.ts`(**chat-task-workbench 车道**),
没有更新 chat-read 车道这 6 条。合入时 chat-read 没跑(见 #2084)。

建议修法(二选一,需要人类裁决走哪条):
- (i) 回退到常显非模态:`RestoredRunApproval` 里把 `ToolPermissionCard` 直接内联进 section,
不套 `Dialog`;`ToolPermissionCard` 的「本 run 内都允许」与契约值 `run` 对齐后统一改回「本任务内允许」,
6 条 spec 一行不改就绿。
- (ii) 确认模态是新验收形态:那 `copilotkit-v2-hitl-dialog-dismiss` 这条不变量必须由人类显式废止,
6 条 spec 改成按 `chat-tool-permission-dialog` 定位,并补一条新的遮罩可接受性验收。

我倾向 (i):A11y issue #2950 的诉求是「弹窗焦点回落」,不需要把常显审批改成模态。

---

## B 组(1 条)—— #2927 主动删掉了 `write_todos` 的卡片渲染

引入点:**`f9afc3d63` / PR #2927(issue #2919)**。同一个 commit 在两处都把 `write_todos` 过滤掉了:
- `copilotkit-v2-assistant-message.tsx` 的 `V2ToolCallsView`:
`allToolCalls.filter(… && call.function.name !== "write_todos")`
- `workbench/task-timeline.tsx` 的 `renderExecutionTool`:`toolName === "write_todos" ? null : …`

于是 `copilotkit-v2-tool-write-todos` 这个 testid 在**任何**路径下都不再挂载。

受害:`copilotkit-v2-tool-rendering.spec.ts:123`,首错逐字是
`attempt 4: copilotkit-v2-tool-write-todos never attached`。

**判定 (b) 测试过期 + (a) 半个回归**。改动理由(计划以持久 plan ledger 为唯一投影,避免重复卡片,
回指 #2451)站得住;但 `f9afc3d63` 同时给 `copilotkit-v2-plan-control.tsx` 加了
`if (["done","cancelled"].includes(ledger.phase) && !hasPlanAction) return null;`
——**计划完成后 ledger 也不渲染了**。两处一起意味着一轮任务结束后,用户在界面上
看不到任何计划痕迹。请确认这是不是本意;若不是,B 组就是真回归而不是测试过期。

修法:spec 改成断言 plan ledger 的 testid(若确认 ledger 保留),或恢复完成态 ledger。

---

## C 组(2 条)—— #2927 自带的新 spec 合入即红,测的是它自己刚交付的功能

`f9afc3d63` 新增 `agent-task-clarification-result.spec.ts` / `agent-task-planning-hitl.spec.ts`
并把它们加进 `playwright.chat-read.config.ts` 的 `testMatch`。这两条**从未绿过**
(合入时 chat-read 车道没跑),所以「在 `d30ac48e8` 是绿的」对它们不成立。

- `agent-task-clarification-result.spec.ts:25`:期望 1 个 `fill_run_params` 的 `tool_start`,
实际 2 个。日志里两条同 `sourceToolCallId`、不同 `attemptId`(`…:1` 与 `…:4`)——
**补参恢复被投影成了第二次 tool_start,而不是原调用的结果**。执行日志投影缺陷。
- `agent-task-planning-hitl.spec.ts:211`:run 已到 `failed`(第 228 行断言通过),
但 `GET /agent-runs/:id` 仍返回非空 `pendingApproval`(choose_execution_option)。
**HITL_REJECTED 终态没有清掉 pending 请求**。
同文件 `confirm_task_intent` 的 :132/:158 都绿,说明只有 `choose_execution_option` 这条路径漏了。

**判定 (a) 产品缺陷**,且是 #2927 刚交付功能上的缺陷,不是历史债。

---

## D 组(1 条)—— `chat_message_id` 映射事件退化成自反

`copilotkit-v2-roster-landing.spec.ts:191`:
期望 `mapping.streamingMessageId === TEXT_MESSAGE_START.messageId`(`0935bb94…:1:assistant`),
实际拿到 `f7ee229d…`,即 `chatMessageId` 本身。

根因链:`copilotkit-agui.controller.ts:916` `messageId = executionRelay.finish(outcome.messageId, …)`
覆盖了流式 id,第 949 行随后用被覆盖的值发 `chat_message_id`。
`execution-journal-relay.ts:96-105` 的 `finish()` 在
「`finalMessageId` 未见过 / 文本字节不等」时会重发一段 `TEXT_MESSAGE_START/CONTENT/END`
并返回 `persistedMessageId`——此时映射事件的两个字段相等,CK-P3(#2064) 的关联事件失去意义。

引入点:`fe7edbd5c`「fix(workbench): restore authoritative final replies after streaming」,
经 `7fc167c0f` / **PR #2869** 进 main。**这个 PR 不在派工给我的候选名单里**,是读代码新找出来的。

**判定 (a) 产品缺陷**(契约事件退化),修法很小:控制器保留流式 id 的副本,
第 949 行发 `streamingMessageId: streamedMessageId`(`finish()` 之前的值),
`finish()` 的返回值只用于后续 `TEXT_MESSAGE_END`。

---

## E 组(6 条)—— 尚未定性,需要 CI 反证

| spec | 首错 | 现状 |
|---|---|---|
| `copilotkit-v2-error-banner.spec.ts:60` | `copilotkit-v2-error` 45s 内 count 0 | loopback 的 `FAILURE_TRIGGER` 分支(provider:494)在区间内**未被改动**,读代码找不到引入点 |
| `copilotkit-v2-message-actions.spec.ts:142` | 同上,60s | 同上,与前一条同因 |
| `copilotkit-v2-tool-rendering.spec.ts:274` ×2 | `活动必须在运行流结束前可见` | 疑似 `8a3834af7`(#2956) 给 `RunTracePanel` 加了 `SubtaskRunLivePanel` 后的时序变化,**无证据** |
| `agent-workbench-steering-acceptance.spec.ts:10` | 30s 内轮询不到 active `tool_start` | 固定 30s 预算,满载 shard 上疑似 #2968 家族,**必须重跑判定** |
| `copilotkit-v2-runtime-adapter.spec.ts:309` | `chat-ai-markdown mounted but empty text` | 症状与组 2「v2 正文恒空」一致,可能应归组 2 |

E 组我不做代码推断结论。已在 `bisect/chat-read-2927`(= `f9afc3d63`,#2927 合入点)
上 dispatch 了一趟 chat-read:
run [34177112918](https://github.com/boardx/workspacex/actions/runs/34177112918)。
它能把 A 组(#2948 之前)与 B/C 组(#2927 之后)一刀切开,并给 E 组一个同装置对照。

---

## 证据强度声明

- A 组、B 组、C 组、D 组:**代码级确定**(diff 显示被断言的 DOM/字段确实被移除或改名),
不依赖 CI 重跑。A 组的三条 `Test timeout` 也不例外——call log 指名在等已被删掉的按钮。
- E 组:**只有症状,没有引入点**。不重跑不下结论。
- 全文没有一条结论来自「重跑绿」,因为还没跑完;跑完会回帖补。

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by running the listed chat-read specs and reviewing the named files: restored-run-approval.tsx, tool-permission-card.tsx, copilotkit-v2-assistant-message.tsx, task-timeline.tsx, copilotkit-agui.controller.ts, and execution-journal-relay.ts. Use the dispatched bisect run to classify E-group failures before changing code. Done means A–D contracts are restored or their tests and UX requirements are explicitly revised, while E has a confirmed cause.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
backend-api-design, frontend, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.