work-ally 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +110 -0
- package/DASHBOARD.md +160 -0
- package/PRODUCT.md +113 -0
- package/README.md +403 -0
- package/ally.sh +171 -0
- package/bridge/src/approval-rules.ts +360 -0
- package/bridge/src/channel-delivery.ts +207 -0
- package/bridge/src/channel-types.ts +22 -0
- package/bridge/src/channels/fake/adapter.ts +31 -0
- package/bridge/src/channels/feishu/adapter.ts +411 -0
- package/bridge/src/channels/feishu/approvals.ts +6 -0
- package/bridge/src/channels/feishu/formatter.ts +276 -0
- package/bridge/src/channels/feishu/normalize.ts +368 -0
- package/bridge/src/codex-config.ts +52 -0
- package/bridge/src/config.ts +240 -0
- package/bridge/src/fake-runtime-client.ts +505 -0
- package/bridge/src/handoff-service.ts +494 -0
- package/bridge/src/logger.ts +194 -0
- package/bridge/src/memory-digest.ts +186 -0
- package/bridge/src/receiver-approval-autonomy.ts +158 -0
- package/bridge/src/receiver-control-core.ts +140 -0
- package/bridge/src/receiver-control-work-session.ts +218 -0
- package/bridge/src/receiver-control.ts +83 -0
- package/bridge/src/receiver-delivery.ts +136 -0
- package/bridge/src/receiver-helpers.ts +96 -0
- package/bridge/src/receiver-human-gate.ts +333 -0
- package/bridge/src/receiver-inbound-preflight.ts +162 -0
- package/bridge/src/receiver-recovery.ts +236 -0
- package/bridge/src/receiver-runtime-callbacks.ts +367 -0
- package/bridge/src/receiver-runtime-policy.ts +132 -0
- package/bridge/src/receiver-runtime-state.ts +124 -0
- package/bridge/src/receiver-support-actions.ts +189 -0
- package/bridge/src/receiver-thread-start.ts +57 -0
- package/bridge/src/receiver-turn-coordination.ts +94 -0
- package/bridge/src/receiver-turn-execution.ts +257 -0
- package/bridge/src/receiver-turn-failure.ts +143 -0
- package/bridge/src/receiver-turn-result.ts +185 -0
- package/bridge/src/receiver-turn-steer.ts +70 -0
- package/bridge/src/receiver-work-session.ts +76 -0
- package/bridge/src/receiver.ts +329 -0
- package/bridge/src/router.ts +62 -0
- package/bridge/src/runtime-client-agent-messages.ts +150 -0
- package/bridge/src/runtime-client-message-dispatch.ts +176 -0
- package/bridge/src/runtime-client-protocol.ts +411 -0
- package/bridge/src/runtime-client-request-ops.ts +56 -0
- package/bridge/src/runtime-client-run-turn.ts +158 -0
- package/bridge/src/runtime-client-thread-ops.ts +270 -0
- package/bridge/src/runtime-client-transport.ts +309 -0
- package/bridge/src/runtime-client-turn-poll.ts +224 -0
- package/bridge/src/runtime-client-turn-read.ts +185 -0
- package/bridge/src/runtime-client-turn-state.ts +105 -0
- package/bridge/src/runtime-client.ts +344 -0
- package/bridge/src/runtime-user-input.ts +403 -0
- package/bridge/src/scheduler.ts +239 -0
- package/bridge/src/server-handoff-command.ts +364 -0
- package/bridge/src/server-main.ts +80 -0
- package/bridge/src/server-routine-command.ts +60 -0
- package/bridge/src/server-routine-execution.ts +222 -0
- package/bridge/src/server-runtime-app-support.ts +107 -0
- package/bridge/src/server-runtime-app.ts +238 -0
- package/bridge/src/server-thread-sync-command.ts +63 -0
- package/bridge/src/server.ts +17 -0
- package/bridge/src/session-store-delivery.ts +220 -0
- package/bridge/src/session-store-human-gate.ts +380 -0
- package/bridge/src/session-store-inbound-acceptance.ts +66 -0
- package/bridge/src/session-store-meta.ts +134 -0
- package/bridge/src/session-store-turn-ledger.ts +272 -0
- package/bridge/src/session-store.ts +380 -0
- package/bridge/src/system-notify.ts +220 -0
- package/bridge/src/thread-sync.ts +200 -0
- package/bridge/src/translator.ts +494 -0
- package/bridge/src/types.ts +289 -0
- package/bridge/src/utils.ts +104 -0
- package/bridge/src/work-session-store.ts +471 -0
- package/docs/.gitkeep +0 -0
- package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
- package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
- package/docs/completed/README.md +21 -0
- package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
- package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
- package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
- package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
- package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
- package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
- package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
- package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
- package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
- package/docs/developer-workflow.md +49 -0
- package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
- package/docs/implementation/test-coverage-map.md +363 -0
- package/docs/implementation/work-ally-implementation-guide.md +790 -0
- package/docs/issues/README.md +10 -0
- package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
- package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
- package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
- package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
- package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
- package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
- package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
- package/docs/manual-acceptance.md +127 -0
- package/docs/ops-runbook.md +44 -0
- package/docs/planning/FEATURE-memory-system.md +748 -0
- package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
- package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
- package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
- package/docs/planning/SPEC-assistant-rename.md +610 -0
- package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
- package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
- package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
- package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
- package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
- package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
- package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
- package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
- package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
- package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
- package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
- package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
- package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
- package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
- package/docs/planning/SPEC-stable-archive-contract.md +456 -0
- package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
- package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
- package/docs/planning/ally-next.md +1278 -0
- package/docs/planning/assistant-workbench-spec.md +725 -0
- package/docs/planning/product-workbench.md +283 -0
- package/docs/product-onboarding.md +227 -0
- package/docs/product-spec-standard.md +528 -0
- package/docs/troubleshooting.md +45 -0
- package/docs/user-quickstart.md +46 -0
- package/internal/dispatch.sh +95 -0
- package/internal/lib/common.sh +1450 -0
- package/internal/modules/assistant/manage.sh +1312 -0
- package/internal/modules/bootstrap/setup.sh +144 -0
- package/internal/modules/config/init-env.sh +10 -0
- package/internal/modules/global/manage.sh +154 -0
- package/internal/modules/handoff/manage.sh +54 -0
- package/internal/modules/mcp/manage.sh +83 -0
- package/internal/modules/ops/logs.sh +76 -0
- package/internal/modules/routines/manage.sh +55 -0
- package/internal/modules/runtime/assistant-autosave.sh +26 -0
- package/internal/modules/runtime/restart.sh +6 -0
- package/internal/modules/runtime/start.sh +283 -0
- package/internal/modules/runtime/status.sh +194 -0
- package/internal/modules/runtime/stop.sh +55 -0
- package/internal/modules/runtime/supervisor.sh +216 -0
- package/internal/modules/runtime/update.sh +26 -0
- package/package.json +41 -0
- package/runtime/config/.gitkeep +0 -0
- package/runtime/host/.gitkeep +0 -0
- package/runtime/host/healthcheck-codex-app-server.ts +22 -0
- package/runtime/host/ping-pong-codex-app-server.ts +66 -0
- package/runtime/host/probe-codex-app-server.ts +115 -0
- package/skills/archive-reader/SKILL.md +9 -0
- package/skills/feishu-production-debug/SKILL.md +37 -0
- package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
- package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
- package/skills/issue-to-spec-triage/SKILL.md +44 -0
- package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
- package/skills/memory-digest/SKILL.md +9 -0
- package/skills/post-implementation-closure/SKILL.md +39 -0
- package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
- package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
- package/skills/product-spec/SKILL.md +244 -0
- package/templates/env.example +5 -0
- package/templates/routines/nightly-memory-digest.yaml +10 -0
- package/templates/workspace/AGENTS.md +26 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 反思报告:为什么“等待审批已可见但审批实体缺失”没有在自测中暴露
|
|
2
|
+
|
|
3
|
+
更新时间:2026-03-16
|
|
4
|
+
状态:反思完成 / 待补测试门禁
|
|
5
|
+
作者:Codex
|
|
6
|
+
关联问题:`docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md`
|
|
7
|
+
|
|
8
|
+
## 1. 这份反思报告回答什么
|
|
9
|
+
|
|
10
|
+
这份文档不重复讲 bug 本身,而是专门回答一个更关键的问题:
|
|
11
|
+
|
|
12
|
+
> 为什么这类问题没有在本地自测里被我发现,反而要等到用户在飞书真实对话里才暴露?
|
|
13
|
+
|
|
14
|
+
目标是把“测试为什么失真”讲清楚,并反推后续测试纪律。
|
|
15
|
+
|
|
16
|
+
## 2. 先说结论
|
|
17
|
+
|
|
18
|
+
这次不是“完全没写审批测试”,而是:
|
|
19
|
+
|
|
20
|
+
- 我写了审批 happy path;
|
|
21
|
+
- 我也测了审批后继续执行;
|
|
22
|
+
- 但我没有把“等待审批状态”和“审批卡已成功交付”拆成两个独立事实去测。
|
|
23
|
+
|
|
24
|
+
所以现有测试验证的是:
|
|
25
|
+
|
|
26
|
+
- 有审批请求时,审批卡能不能工作;
|
|
27
|
+
|
|
28
|
+
却没有验证:
|
|
29
|
+
|
|
30
|
+
- 只有 thread flag、没有审批实体时,系统是不是还会错误进入 waiting state;
|
|
31
|
+
- 后续自然语言是不是会被误拦截;
|
|
32
|
+
- 没有审批卡时,系统是不是仍错误要求用户审批。
|
|
33
|
+
|
|
34
|
+
这正是现实现场里发生的事。
|
|
35
|
+
|
|
36
|
+
## 3. 当时有哪些测试,为什么它们没拦住
|
|
37
|
+
|
|
38
|
+
当前仓库里,和审批最相关的测试主要是:
|
|
39
|
+
|
|
40
|
+
- `tests/integration/session/approval-flow.test.mjs`
|
|
41
|
+
- `tests/integration/session/waiting-and-redelivery.test.mjs`
|
|
42
|
+
- `tests/integration/session/control-commands.test.mjs`
|
|
43
|
+
|
|
44
|
+
这些测试覆盖了:
|
|
45
|
+
|
|
46
|
+
- 正常审批卡生成;
|
|
47
|
+
- 回复审批卡“同意/拒绝”;
|
|
48
|
+
- `/approve <id>` 命令;
|
|
49
|
+
- 阻塞期间追问会被拦截;
|
|
50
|
+
- 长时间等待审批不应直接失败。
|
|
51
|
+
|
|
52
|
+
问题在于,这些测试都共享一个默认前提:
|
|
53
|
+
|
|
54
|
+
> 只要 runtime 进入 `waitingOnApproval`,审批卡实体一定也已经存在。
|
|
55
|
+
|
|
56
|
+
这个前提在假 runtime 里总是成立,所以整个测试世界里,“等待审批但没有审批卡”这种状态根本不可能出现。
|
|
57
|
+
|
|
58
|
+
## 4. 5 Why 反思
|
|
59
|
+
|
|
60
|
+
### Why 1:为什么自测没暴露这个 bug?
|
|
61
|
+
|
|
62
|
+
因为测试只验证了“审批卡存在时审批是否可用”,没有验证“审批卡缺失时系统是否仍错误阻塞”。
|
|
63
|
+
|
|
64
|
+
### Why 2:为什么没有覆盖“审批卡缺失”这个场景?
|
|
65
|
+
|
|
66
|
+
因为我写测试时默认认为:`waitingOnApproval` 和 `approval_requested` 是同一件事的两个侧面,不会分离。
|
|
67
|
+
|
|
68
|
+
### Why 3:为什么我会做这个默认假设?
|
|
69
|
+
|
|
70
|
+
因为 `FakeRuntimeClient` 的建模把这两个阶段耦合在一起了:一旦进入审批流程,waiting flag 和审批回调会按顺滑路径一起出现。
|
|
71
|
+
|
|
72
|
+
也就是说,测试替身本身把一种真实系统里可能分离的时序,错误建模成了“原子成功”。
|
|
73
|
+
|
|
74
|
+
### Why 4:为什么测试替身会被我建成这样?
|
|
75
|
+
|
|
76
|
+
因为我当时写测试的视角是“功能流程要能走通”,而不是“状态机边界是否稳固”。
|
|
77
|
+
|
|
78
|
+
我验证的是:
|
|
79
|
+
|
|
80
|
+
- 审批后能继续;
|
|
81
|
+
- 拒绝后能失败;
|
|
82
|
+
- 自动审批能放行;
|
|
83
|
+
|
|
84
|
+
但没有从第一性原理问一句:
|
|
85
|
+
|
|
86
|
+
- 用户被阻塞前,是否已经真正拿到了可操作对象?
|
|
87
|
+
|
|
88
|
+
### Why 5:为什么我没有把这个问题上升成测试不变量?
|
|
89
|
+
|
|
90
|
+
因为我的测试设计仍然太贴实现路径,而不是贴产品事实。
|
|
91
|
+
|
|
92
|
+
真正应该守的不变量是:
|
|
93
|
+
|
|
94
|
+
> 用户可见的 `waiting approval` 只能建立在“审批实体已成功交付给用户”之上。
|
|
95
|
+
|
|
96
|
+
这个不变量之前没有被写进测试,所以实现可以静悄悄地偏掉。
|
|
97
|
+
|
|
98
|
+
## 5. 为什么要等到飞书里才暴露
|
|
99
|
+
|
|
100
|
+
因为飞书真实运行态里,审批链路不是一个原子动作,而是至少三段:
|
|
101
|
+
|
|
102
|
+
1. runtime thread 出现 `waitingOnApproval`
|
|
103
|
+
2. bridge 收到并记录审批请求
|
|
104
|
+
3. bridge 成功把审批卡发到用户前台
|
|
105
|
+
|
|
106
|
+
而我的本地自测,把这三段压缩成了一个“同步成功”的假世界。
|
|
107
|
+
|
|
108
|
+
于是:
|
|
109
|
+
|
|
110
|
+
- 在假世界里,用户一旦被提示等待审批,就一定已经能看到审批卡;
|
|
111
|
+
- 在真实世界里,这三步可能时序错位、漏接、延迟、甚至只到了前两步。
|
|
112
|
+
|
|
113
|
+
所以问题只会在真实链路里显形。
|
|
114
|
+
|
|
115
|
+
## 6. 自测设计到底哪里错了
|
|
116
|
+
|
|
117
|
+
这次反映出的测试设计错误有三层。
|
|
118
|
+
|
|
119
|
+
### 6.1 错在按 feature 测,而不是按状态不变量测
|
|
120
|
+
|
|
121
|
+
我测了“审批功能是否可用”,但没测“阻塞态是否有资格成立”。
|
|
122
|
+
|
|
123
|
+
### 6.2 错在替身建模过于乐观
|
|
124
|
+
|
|
125
|
+
`FakeRuntimeClient` 默认让 waiting flag 和审批请求同进同出,掩盖了真实系统的时序裂缝。
|
|
126
|
+
|
|
127
|
+
### 6.3 错在没有对“用户可见事实”做独立断言
|
|
128
|
+
|
|
129
|
+
测试断言大多围绕:
|
|
130
|
+
|
|
131
|
+
- 有没有审批卡
|
|
132
|
+
- 能不能 approve
|
|
133
|
+
- 最终 reply 对不对
|
|
134
|
+
|
|
135
|
+
但没有独立断言:
|
|
136
|
+
|
|
137
|
+
- 如果没有 `approval_requested`,就绝不能进入真正的 waiting interception。
|
|
138
|
+
|
|
139
|
+
## 7. 后续必须补的测试门禁
|
|
140
|
+
|
|
141
|
+
针对这个 issue,后续必须补的不是更多 happy path,而是下面几类 fault-injection case:
|
|
142
|
+
|
|
143
|
+
1. runtime 先给 `waitingOnApproval`,但审批请求事件缺失。
|
|
144
|
+
2. runtime 先给 `waitingOnApproval`,审批请求晚到。
|
|
145
|
+
3. 用户只看到 `waiting approval` 状态时继续追问,消息应继续转给 Codex 或进入“详情同步中”,不能直接拦截。
|
|
146
|
+
4. 没有审批卡上下文时,用户说“同意”,系统不能误判为用户已完成审批,也不能永远卡死。
|
|
147
|
+
5. session/archive 中不存在 pending approval object 时,`waiting_user_follow_up_intercepted` 不得成立。
|
|
148
|
+
|
|
149
|
+
## 8. 以后我的测试纪律要怎么改
|
|
150
|
+
|
|
151
|
+
以后这类问题不能再只测“功能通不通”,必须额外测一层:
|
|
152
|
+
|
|
153
|
+
> 用户面前成立的事实,是否真的有后端证据和前台实体支撑。
|
|
154
|
+
|
|
155
|
+
具体到审批链路,就是:
|
|
156
|
+
|
|
157
|
+
- waiting flag
|
|
158
|
+
- pending approval record
|
|
159
|
+
- approval card 已发送
|
|
160
|
+
- 用户后续消息是否应该被拦截
|
|
161
|
+
|
|
162
|
+
这四件事必须拆开建模,不能再默认捆绑。
|
|
163
|
+
|
|
164
|
+
## 9. 一句话反思
|
|
165
|
+
|
|
166
|
+
这次不是我没测审批,而是我把“审批流程走通”误当成了“审批阻塞模型正确”;以后必须把“用户被阻塞前,是否真的拿到了可操作实体”提升成独立测试不变量。
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# 反思报告:为什么“阻塞态已可见但用户无可操作实体”没有在自测中暴露
|
|
2
|
+
|
|
3
|
+
更新时间:2026-03-16
|
|
4
|
+
状态:反思完成 / 待补测试门禁
|
|
5
|
+
作者:Codex
|
|
6
|
+
关联问题:`docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md`
|
|
7
|
+
|
|
8
|
+
## 1. 这份反思报告回答什么
|
|
9
|
+
|
|
10
|
+
这份文档聚焦更上位的问题:
|
|
11
|
+
|
|
12
|
+
> 为什么我之前已经写了 user input、MCP auto-resolve、waiting state 等测试,但仍然没在本地发现“用户已被阻塞、但根本没有拿到可操作实体”这类问题?
|
|
13
|
+
|
|
14
|
+
这不是单个审批 bug,而是我测试模型本身的缺口。
|
|
15
|
+
|
|
16
|
+
## 2. 先说结论
|
|
17
|
+
|
|
18
|
+
这次自测失效的根本原因是:
|
|
19
|
+
|
|
20
|
+
- 我的测试是按“功能入口”组织的;
|
|
21
|
+
- 真实事故暴露的是“阻塞资格”问题;
|
|
22
|
+
- 我没有把“阻塞态成立”这件事拆成独立 contract 来测。
|
|
23
|
+
|
|
24
|
+
因此,现有测试都在验证:
|
|
25
|
+
|
|
26
|
+
- user input 请求来了后能不能继续;
|
|
27
|
+
- MCP 默认自动放行后能不能跑完;
|
|
28
|
+
- 等待期间能不能恢复;
|
|
29
|
+
|
|
30
|
+
却没有验证:
|
|
31
|
+
|
|
32
|
+
- 如果只有 `waitingOnUserInput` / `waitingOnApproval` flag,而没有用户可见实体,bridge 是否仍错误阻塞;
|
|
33
|
+
- auto-resolve 过程中,waiting flag 是否会先于实体 / 收口结果暴露;
|
|
34
|
+
- `pendingUserInputRequestId` 为空时,为什么还能拦截用户追问。
|
|
35
|
+
|
|
36
|
+
## 3. 当时有哪些测试,为什么它们没拦住
|
|
37
|
+
|
|
38
|
+
相关测试主要包括:
|
|
39
|
+
|
|
40
|
+
- `tests/integration/session/user-input-flow.test.mjs`
|
|
41
|
+
- `tests/integration/session/mcp-tool-approval-flow.test.mjs`
|
|
42
|
+
- `tests/integration/session/waiting-and-redelivery.test.mjs`
|
|
43
|
+
- `tests/unit/runtime/runtime-client-pull-primary.test.mjs`
|
|
44
|
+
|
|
45
|
+
这些测试验证了:
|
|
46
|
+
|
|
47
|
+
- user input 请求能发出来并恢复;
|
|
48
|
+
- MCP allowlist 场景可自动放行;
|
|
49
|
+
- 非 allowlist 的 MCP elicitation 会正常提示用户;
|
|
50
|
+
- waitingOnUserInput / waitingOnApproval 长时间存在时不会超时失败。
|
|
51
|
+
|
|
52
|
+
但它们共同遗漏了一件事:
|
|
53
|
+
|
|
54
|
+
> 没有任何一条测试故意制造“flag 已出现,但用户可操作实体缺失”的失真态。
|
|
55
|
+
|
|
56
|
+
所以整个测试世界默认认为:
|
|
57
|
+
|
|
58
|
+
- 有 waiting flag,就一定有 pending object;
|
|
59
|
+
- 有 pending object,就一定已对用户可见;
|
|
60
|
+
- auto-resolve 也总能在线性时序里完成。
|
|
61
|
+
|
|
62
|
+
而真实现场恰恰打破了这些假设。
|
|
63
|
+
|
|
64
|
+
## 4. 5 Why 反思
|
|
65
|
+
|
|
66
|
+
### Why 1:为什么自测没发现这个家族问题?
|
|
67
|
+
|
|
68
|
+
因为我测的是“审批链路”“user input 链路”“MCP 链路”能否走通,没有测“阻塞态是否具备成立条件”。
|
|
69
|
+
|
|
70
|
+
### Why 2:为什么我没测“阻塞资格”而只测“链路走通”?
|
|
71
|
+
|
|
72
|
+
因为我当时的测试思路仍然是 feature-driven,不是 state-contract-driven。
|
|
73
|
+
|
|
74
|
+
也就是说,我在问:
|
|
75
|
+
|
|
76
|
+
- 用户输入来了能不能继续?
|
|
77
|
+
- 自动放行后能不能完成?
|
|
78
|
+
|
|
79
|
+
而没有问:
|
|
80
|
+
|
|
81
|
+
- 系统有资格拦截用户后续自然语言吗?
|
|
82
|
+
|
|
83
|
+
### Why 3:为什么我没有把“阻塞资格”定义成 contract?
|
|
84
|
+
|
|
85
|
+
因为我默认认为 waiting flag 自身就足够说明“现在轮到用户处理”,没把“用户可操作实体”作为一个独立事实层。
|
|
86
|
+
|
|
87
|
+
### Why 4:为什么会形成这种默认?
|
|
88
|
+
|
|
89
|
+
因为假 runtime、session 状态和 channel 发送,在本地测试里基本总是线性一致的:
|
|
90
|
+
|
|
91
|
+
- status 变了
|
|
92
|
+
- request 也来了
|
|
93
|
+
- prompt 也发了
|
|
94
|
+
- 用户答复后就恢复
|
|
95
|
+
|
|
96
|
+
这种顺滑路径让我忽略了真实系统里的一个关键事实:
|
|
97
|
+
|
|
98
|
+
- status 信号
|
|
99
|
+
- pending object
|
|
100
|
+
- channel delivery
|
|
101
|
+
- auto-resolve result
|
|
102
|
+
|
|
103
|
+
它们并不是天然原子的。
|
|
104
|
+
|
|
105
|
+
### Why 5:为什么我要等飞书里才意识到这个问题?
|
|
106
|
+
|
|
107
|
+
因为只有在真实链路里,这几个层次才会自然拉开:
|
|
108
|
+
|
|
109
|
+
- runtime 可能先给 flag;
|
|
110
|
+
- request 事件可能晚到或漏接;
|
|
111
|
+
- auto-resolve 可能和状态机切换不是同一拍;
|
|
112
|
+
- 用户前台是否真正见到实体,又是第三层事实。
|
|
113
|
+
|
|
114
|
+
而我之前的测试没有主动把这些层次拆开,所以实验室里永远不可能长出这种 bug。
|
|
115
|
+
|
|
116
|
+
## 5. 为什么飞书里才暴露
|
|
117
|
+
|
|
118
|
+
因为飞书现场不是单一模块自娱自乐,而是一个跨层系统:
|
|
119
|
+
|
|
120
|
+
1. runtime 给 thread status
|
|
121
|
+
2. runtime 或 bridge 生成 request / elicitation object
|
|
122
|
+
3. session store 记录 pending object
|
|
123
|
+
4. channel 发到飞书前台
|
|
124
|
+
5. 用户看到后才能真正执行下一步
|
|
125
|
+
|
|
126
|
+
现在回看,我之前的测试覆盖大多停留在:
|
|
127
|
+
|
|
128
|
+
- 第 1 层和第 2 层是否存在;
|
|
129
|
+
- 第 5 层能否在理想前提下完成;
|
|
130
|
+
|
|
131
|
+
却几乎没测:
|
|
132
|
+
|
|
133
|
+
- 第 2、3、4 层之间是否一致;
|
|
134
|
+
- 如果 2/3/4 断裂,bridge 是否还错误拦截第 5 层输入。
|
|
135
|
+
|
|
136
|
+
所以它必须等到真实飞书链路里才爆。
|
|
137
|
+
|
|
138
|
+
## 6. 我当时的自测模型哪里不合理
|
|
139
|
+
|
|
140
|
+
### 6.1 把“状态”误当成“用户事实”
|
|
141
|
+
|
|
142
|
+
我测了 waiting flag,但没测 waiting flag 是否被用户真正兑现成了可操作对象。
|
|
143
|
+
|
|
144
|
+
### 6.2 把 auto-resolve 误当成线性即时成功
|
|
145
|
+
|
|
146
|
+
在 MCP 相关测试里,我主要验证 allowlist 是否能自动放行,但没有刻意验证:
|
|
147
|
+
|
|
148
|
+
- waiting flag 先出现、auto-resolve 结果后到
|
|
149
|
+
- auto-resolve 失败或未落库
|
|
150
|
+
- pending object 为空却仍阻塞
|
|
151
|
+
|
|
152
|
+
### 6.3 没有跨层对账断言
|
|
153
|
+
|
|
154
|
+
测试大多盯住 channel.sent 是否包含某类消息,却没有系统性验证:
|
|
155
|
+
|
|
156
|
+
- runtime flag
|
|
157
|
+
- session pending record
|
|
158
|
+
- archive event
|
|
159
|
+
- channel outbound
|
|
160
|
+
|
|
161
|
+
这四层是否一致。
|
|
162
|
+
|
|
163
|
+
## 7. 后续必须补的测试门禁
|
|
164
|
+
|
|
165
|
+
后续围绕这个上位问题,必须补以下 contract tests:
|
|
166
|
+
|
|
167
|
+
1. 仅有 `waitingOnApproval` / `waitingOnUserInput` flag,没有 pending object。
|
|
168
|
+
2. pending object 已存在,但 channel 未成功发出。
|
|
169
|
+
3. auto-resolve 规则命中前,waiting flag 先暴露。
|
|
170
|
+
4. session 中 `pendingUserInputRequestId` 为 `null` 时,任何 follow-up 都不得走 waiting interception。
|
|
171
|
+
5. 没有审批卡 / 没有 input prompt 时,用户自然语言必须继续转发给 Codex,或走“阻塞详情待同步”而不是本地卡死。
|
|
172
|
+
6. archive、session、channel 三层必须对账:只要缺一层,就不允许进入真正的 `waiting_user_action`。
|
|
173
|
+
|
|
174
|
+
## 8. 以后我的测试纪律要怎么改
|
|
175
|
+
|
|
176
|
+
以后对任何“用户被阻塞”的 feature,都必须先问三个问题:
|
|
177
|
+
|
|
178
|
+
1. 系统内部为什么认为现在轮到用户操作?
|
|
179
|
+
2. 用户前台到底拿到了什么可执行实体?
|
|
180
|
+
3. 如果实体没拿到,bridge 是否还会错误地拦截用户输入?
|
|
181
|
+
|
|
182
|
+
这三个问题必须被单独测到,不能只靠 happy path 测试间接覆盖。
|
|
183
|
+
|
|
184
|
+
## 9. 一句话反思
|
|
185
|
+
|
|
186
|
+
这次不是某条链路单独漏测,而是我把“waiting flag 成立”误当成了“用户阻塞条件成立”;以后必须把“阻塞资格”本身定义成一等测试对象。
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 反思报告:为什么“fresh thread 过早终态判定与 `[object Object]` 外泄”没有在自测中暴露
|
|
2
|
+
|
|
3
|
+
更新时间:2026-03-16
|
|
4
|
+
状态:反思完成 / 待补测试门禁
|
|
5
|
+
作者:Codex
|
|
6
|
+
关联问题:`docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md`
|
|
7
|
+
|
|
8
|
+
## 1. 这份反思报告回答什么
|
|
9
|
+
|
|
10
|
+
这份文档专门回答:
|
|
11
|
+
|
|
12
|
+
> 为什么我已经给 `runtime-client` 写了 pull-primary、transient read failure、late terminal result 等测试,结果 `[object Object]` 这种事故还是只能等飞书里真实对话才暴露?
|
|
13
|
+
|
|
14
|
+
这次必须反思的,不是“少写了一条 case”这么简单,而是我的 turn 终态测试模型本身还不够完整。
|
|
15
|
+
|
|
16
|
+
## 2. 先说结论
|
|
17
|
+
|
|
18
|
+
这次没被自测提前拦住,根因有三条:
|
|
19
|
+
|
|
20
|
+
- 我测了“通用 transient error”,没测“协议级结构化错误对象”;
|
|
21
|
+
- 我测了“thread/read 暂时断连”,没测“fresh thread 刚启动时尚未 materialize”;
|
|
22
|
+
- 我测了“completed event 与 poll 冲突”,没测“poll 先错误 reject,导致 completed event 来晚后已经救不回来”。
|
|
23
|
+
|
|
24
|
+
所以现有测试确实覆盖了一部分恢复逻辑,但没有覆盖这次事故真正命中的那块状态空间。
|
|
25
|
+
|
|
26
|
+
## 3. 当时有哪些测试,为什么它们没拦住
|
|
27
|
+
|
|
28
|
+
相关测试主要在:
|
|
29
|
+
|
|
30
|
+
- `tests/unit/runtime/runtime-client-pull-primary.test.mjs`
|
|
31
|
+
- `tests/unit/runtime/runtime-client-lifecycle.test.mjs`
|
|
32
|
+
- `tests/integration/session/runtime-failure-and-recovery.test.mjs`
|
|
33
|
+
|
|
34
|
+
这些测试已经覆盖了:
|
|
35
|
+
|
|
36
|
+
- `thread/read` 遇到 `runtime websocket is not connected` 的重试;
|
|
37
|
+
- pull-primary 在 `turn/completed` 不到时仍可从 `thread/read` 恢复;
|
|
38
|
+
- `turn/completed` 与后续 poll 冲突时优先用 authoritative completed result;
|
|
39
|
+
- bounded timeout 后的错误提示。
|
|
40
|
+
|
|
41
|
+
但它们没有覆盖这次事故的三个关键条件同时成立:
|
|
42
|
+
|
|
43
|
+
1. 这是一个 fresh thread;
|
|
44
|
+
2. 首次 `thread/read(includeTurns=true)` 返回的是结构化 JSON-RPC object error;
|
|
45
|
+
3. 这个错误既不在 recoverable regex 名单里,又不是 turn 真失败;
|
|
46
|
+
4. runtime 后面仍会给出 `turn/completed`,但因为前面已 reject,后续事实无法再兑现给用户。
|
|
47
|
+
|
|
48
|
+
少了这组组合,整个测试看起来就像“恢复逻辑已经够用了”。
|
|
49
|
+
|
|
50
|
+
## 4. 5 Why 反思
|
|
51
|
+
|
|
52
|
+
### Why 1:为什么自测没暴露这次 `[object Object]`?
|
|
53
|
+
|
|
54
|
+
因为我没有构造“结构化错误对象 + fresh thread materialization 窗口”这一类场景。
|
|
55
|
+
|
|
56
|
+
### Why 2:为什么没有构造这类场景?
|
|
57
|
+
|
|
58
|
+
因为我写 runtime 测试时,主要围绕已知 recoverable 文本错误来测,比如:
|
|
59
|
+
|
|
60
|
+
- `runtime websocket is not connected`
|
|
61
|
+
|
|
62
|
+
而不是围绕协议语义来测:
|
|
63
|
+
|
|
64
|
+
- JSON-RPC error object
|
|
65
|
+
- not materialized yet
|
|
66
|
+
- includeTurns unavailable before first user message
|
|
67
|
+
|
|
68
|
+
### Why 3:为什么我会更偏向文本错误而不是协议语义?
|
|
69
|
+
|
|
70
|
+
因为我当时的测试思路还是实现驱动的:
|
|
71
|
+
|
|
72
|
+
- 代码里正则匹配什么,我就测什么;
|
|
73
|
+
- 代码里已有分支是什么,我就补什么。
|
|
74
|
+
|
|
75
|
+
这会导致一个问题:
|
|
76
|
+
|
|
77
|
+
> 测试在证明“当前实现能处理它已经认识的错误”,而不是证明“系统能覆盖完整的协议失效面”。
|
|
78
|
+
|
|
79
|
+
### Why 4:为什么 fresh thread materialization 这类状态被我漏掉了?
|
|
80
|
+
|
|
81
|
+
因为我的假设是:
|
|
82
|
+
|
|
83
|
+
- `turn/start` 成功后,`thread/read(includeTurns=true)` 基本立刻可用。
|
|
84
|
+
|
|
85
|
+
这个假设在大多数 stub / fake 环境里都成立,所以我没有主动质疑它。
|
|
86
|
+
|
|
87
|
+
但真实 runtime 明确告诉我们:
|
|
88
|
+
|
|
89
|
+
- fresh thread 在某个短窗口里,`includeTurns` 可能还不可用。
|
|
90
|
+
|
|
91
|
+
这说明我之前的测试模型把 thread 生命周期过度理想化了。
|
|
92
|
+
|
|
93
|
+
### Why 5:为什么最终要等飞书里才看见这个事故?
|
|
94
|
+
|
|
95
|
+
因为只有真实链路会同时出现这四层组合:
|
|
96
|
+
|
|
97
|
+
1. 新 thread 刚建好;
|
|
98
|
+
2. poll 立即开始;
|
|
99
|
+
3. 协议返回结构化对象错误;
|
|
100
|
+
4. 后续 turn 其实继续执行并完成。
|
|
101
|
+
|
|
102
|
+
而本地自测之前从没主动制造过“先误判失败、后真实完成”的窗口,所以系统提前 reject 的问题一直被藏住了。
|
|
103
|
+
|
|
104
|
+
## 5. 为什么飞书里才暴露
|
|
105
|
+
|
|
106
|
+
因为飞书现场不是只测 runtime-client 单点,而是把整条链串起来了:
|
|
107
|
+
|
|
108
|
+
- 用户发消息
|
|
109
|
+
- bridge start thread / turn
|
|
110
|
+
- pollTurnFinalState 立即启动
|
|
111
|
+
- receiver 把 reject 直接翻成用户错误
|
|
112
|
+
- 后面真实的 `turn/completed` 再到来时,已经晚了
|
|
113
|
+
|
|
114
|
+
在 unit test 里,如果没把这整个序列连起来,只测其中一段,很容易得出“恢复逻辑整体没问题”的错觉。
|
|
115
|
+
|
|
116
|
+
## 6. 我当时的自测模型哪里不合理
|
|
117
|
+
|
|
118
|
+
### 6.1 用实现当前认得的错误,代替协议层完整错误面
|
|
119
|
+
|
|
120
|
+
我测了 regex 已识别的错误,却没测 JSON-RPC object error 的归一化。
|
|
121
|
+
|
|
122
|
+
### 6.2 把 fresh thread 生命周期理想化了
|
|
123
|
+
|
|
124
|
+
我默认 `turn/start` 之后就能立即安全 `thread/read(includeTurns=true)`,没有给 materialization 窗口留位置。
|
|
125
|
+
|
|
126
|
+
### 6.3 没测“过早 reject 后 completed event 迟到”的致命组合
|
|
127
|
+
|
|
128
|
+
我测了 completed event 与 poll 冲突,但没测:
|
|
129
|
+
|
|
130
|
+
- poll 先把 turn state finalize 掉;
|
|
131
|
+
- completed event 后到;
|
|
132
|
+
- 结果因为 turn 已被打死,真实完成事实无法再交付。
|
|
133
|
+
|
|
134
|
+
这恰好就是这次事故的致命点。
|
|
135
|
+
|
|
136
|
+
### 6.4 没有用户面向断言
|
|
137
|
+
|
|
138
|
+
之前测试更多断言 runtime 结果对象,而不是最终用户面向输出,所以“用户看到 `[object Object]`”这种问题没有被单独设成红线。
|
|
139
|
+
|
|
140
|
+
## 7. 后续必须补的测试门禁
|
|
141
|
+
|
|
142
|
+
针对这个 issue,后续至少必须补:
|
|
143
|
+
|
|
144
|
+
1. `turn/start` 后首次 `thread/read(includeTurns=true)` 返回 JSON-RPC object error。
|
|
145
|
+
2. 错误内容是 `thread ... is not materialized yet`,并应被判为暂态而不是终态失败。
|
|
146
|
+
3. 结构化错误对象必须被正确归一化,用户绝不能看到 `[object Object]`。
|
|
147
|
+
4. 暂态读错误发生后,系统不得提前记录 `turn_failed`。
|
|
148
|
+
5. 同一轮在暂态错误后 later `turn/completed` 到来时,结果仍应能成功交付。
|
|
149
|
+
6. integration 层必须断言:用户最终看到的是正常 final reply 或明确恢复语义,而不是裸内部错误。
|
|
150
|
+
|
|
151
|
+
## 8. 以后我的测试纪律要怎么改
|
|
152
|
+
|
|
153
|
+
以后对 turn 终态相关测试,不能只按“transport 断开 / 超时 / completed conflict”去列,要按 MECE 的终态风险面去列:
|
|
154
|
+
|
|
155
|
+
- transport 错误
|
|
156
|
+
- protocol 错误
|
|
157
|
+
- thread lifecycle 可见性
|
|
158
|
+
- event vs poll 时序冲突
|
|
159
|
+
- 过早 terminalization
|
|
160
|
+
- 用户出口错误外泄
|
|
161
|
+
|
|
162
|
+
只有这样,测试才不会一直围着当前实现的小圆圈转。
|
|
163
|
+
|
|
164
|
+
## 9. 一句话反思
|
|
165
|
+
|
|
166
|
+
这次不是我没写恢复测试,而是我把“恢复”理解得太像传输层问题,没把 fresh thread 生命周期、协议对象错误和过早终态化当成同一组必须联测的风险面。
|