@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.0

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.
Files changed (166) hide show
  1. package/README.md +39 -12
  2. package/agents/analyst.md +61 -0
  3. package/agents/coder.md +70 -0
  4. package/agents/debugger.md +67 -0
  5. package/agents/doc-reviewer.md +3 -3
  6. package/agents/explorer.md +50 -18
  7. package/agents/general-purpose.md +19 -8
  8. package/agents/orchestrator.md +37 -32
  9. package/agents/planner.md +45 -11
  10. package/agents/researcher.md +53 -11
  11. package/agents/reviewer.md +74 -0
  12. package/package.json +1 -1
  13. package/skills/workflow-script-format/SKILL.md +1 -1
  14. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  15. package/src/execution/__tests__/agent-registry.test.ts +13 -11
  16. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  17. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  18. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  19. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  20. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  21. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  22. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  24. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  25. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  26. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  27. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  28. package/src/execution/__tests__/execution-record.test.ts +199 -0
  29. package/src/execution/__tests__/finalize-record.test.ts +170 -14
  30. package/src/execution/__tests__/format.test.ts +131 -7
  31. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  32. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  33. package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
  34. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  35. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  36. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  37. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  38. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  39. package/src/execution/__tests__/list-component.test.ts +59 -5
  40. package/src/execution/__tests__/list-fields.test.ts +109 -0
  41. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  42. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  43. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  44. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  45. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  46. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  47. package/src/execution/__tests__/record-store.test.ts +158 -52
  48. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  49. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  50. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  51. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  52. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  53. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  54. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  55. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  56. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  57. package/src/execution/__tests__/session-pending.test.ts +61 -2
  58. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  59. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  60. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  61. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  62. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  63. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  64. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  65. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  66. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  67. package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
  68. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  69. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  70. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  71. package/src/execution/__tests__/tool-action.test.ts +12 -10
  72. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  73. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  74. package/src/execution/__tests__/worktree-manager.test.ts +292 -89
  75. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
  76. package/src/execution/argv-mirror.ts +21 -2
  77. package/src/execution/execution-record.ts +126 -9
  78. package/src/execution/finalize-record.ts +90 -13
  79. package/src/execution/host-mode.ts +1 -1
  80. package/src/execution/lifecycle-manager.ts +484 -0
  81. package/src/execution/lifecycle-predicates.ts +65 -0
  82. package/src/execution/manifest-store.ts +61 -16
  83. package/src/execution/model-resolver.ts +26 -5
  84. package/src/execution/notifier.ts +69 -12
  85. package/src/execution/pi-invocation.ts +21 -1
  86. package/src/execution/record-store.ts +554 -107
  87. package/src/execution/session-pending.ts +116 -45
  88. package/src/execution/session-reconstructor.ts +224 -7
  89. package/src/execution/session-runner.ts +290 -75
  90. package/src/execution/sessions-index.ts +304 -0
  91. package/src/execution/stdin-writer.ts +93 -7
  92. package/src/execution/stream-sink.ts +20 -3
  93. package/src/execution/subagent-service.ts +867 -138
  94. package/src/execution/turn-limiter.ts +14 -0
  95. package/src/execution/types.ts +204 -22
  96. package/src/execution/worktree-manager.ts +128 -49
  97. package/src/execution/worktree-registry.ts +13 -2
  98. package/src/index.ts +277 -19
  99. package/src/injectors/subagent-list-injector.ts +26 -8
  100. package/src/injectors/workflow-list-injector.ts +25 -8
  101. package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
  102. package/src/interface/__tests__/tool-render.test.ts +15 -13
  103. package/src/interface/bg-notify-render.ts +32 -8
  104. package/src/interface/command-actions.ts +26 -7
  105. package/src/interface/commands.ts +21 -22
  106. package/src/interface/format.ts +44 -17
  107. package/src/interface/gui-mappers.ts +6 -8
  108. package/src/interface/helpers.ts +170 -10
  109. package/src/interface/list-component.ts +53 -14
  110. package/src/interface/subagent-actions.ts +235 -17
  111. package/src/interface/subagent-tool.ts +82 -17
  112. package/src/interface/subagents.ts +2 -1
  113. package/src/interface/tool-render.ts +11 -24
  114. package/src/interface/tool-workflow.ts +20 -35
  115. package/src/interface/views/WorkflowsView.ts +89 -32
  116. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  117. package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
  118. package/src/interface/views/format.ts +3 -3
  119. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
  120. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  121. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  122. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  123. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  124. package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  128. package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
  129. package/src/orchestration/__tests__/test-mocks.ts +191 -0
  130. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
  131. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  132. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  133. package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
  134. package/src/orchestration/agent-opts-resolver.ts +4 -1
  135. package/src/orchestration/args-validator.ts +2 -2
  136. package/src/orchestration/config-loader.ts +30 -1
  137. package/src/orchestration/error-recovery.ts +133 -29
  138. package/src/orchestration/execute-agent-call.ts +31 -7
  139. package/src/orchestration/jsonl-run-store.ts +287 -40
  140. package/src/orchestration/launcher.ts +7 -1
  141. package/src/orchestration/lifecycle.ts +135 -132
  142. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  143. package/src/orchestration/models/budget.ts +1 -1
  144. package/src/orchestration/models/run-runtime.ts +15 -17
  145. package/src/orchestration/models/run-spec.ts +2 -2
  146. package/src/orchestration/models/run-state.ts +3 -3
  147. package/src/orchestration/models/trace.ts +95 -15
  148. package/src/orchestration/models/types.ts +8 -9
  149. package/src/orchestration/models/workflow-run.ts +50 -71
  150. package/src/orchestration/models/workflow-script.ts +32 -1
  151. package/src/orchestration/skill-discovery.ts +30 -0
  152. package/src/orchestration/worker-handle.ts +1 -1
  153. package/src/orchestration/worker-host.ts +1 -1
  154. package/src/orchestration/worker-script-builder.ts +29 -10
  155. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  156. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  157. package/src/shared/__tests__/resource-discovery.test.ts +55 -0
  158. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  159. package/src/shared/agent-ref.ts +16 -0
  160. package/src/shared/resource-discovery.ts +147 -58
  161. package/src/shared/schema-jsonify.ts +53 -0
  162. package/workflows/README.md +4 -4
  163. package/agents/code-reviewer.md +0 -47
  164. package/agents/context-builder.md +0 -21
  165. package/agents/oracle.md +0 -34
  166. package/agents/worker.md +0 -20
@@ -0,0 +1,345 @@
1
+ // src/execution/__tests__/status-refactor.test.ts
2
+ //
3
+ // SP-1: L1/L2 状态机重构验证测试。
4
+ //
5
+ // 验证:
6
+ // 1. ClosedReason 枚举完整性(6 个值)
7
+ // 2. tryTransition → closed + closedReason 写入
8
+ // 3. completeRecord → closed + closedReason 写入
9
+ // 4. notifier dedupKey: round 参与去重(MF-1 修复,idle 按 id:round 去重)
10
+ // 5. ExecutionStatus 不含 done/failed/crashed 字面量
11
+ // 6. mapExternalState closed → ended
12
+ // 7. statusGlyph closed → ✓ success
13
+ // 8. BgNotifyRecord.status 不含 done/failed
14
+
15
+ import { describe, expect, it } from "vitest";
16
+
17
+ import { completeRecord, createRecord, tryTransition } from "../execution-record.ts";
18
+ import { BgNotifier } from "../notifier.ts";
19
+ import type { BgNotifyRecord, NotifierHost } from "../notifier.ts";
20
+ import { mapExternalState } from "../../interface/subagent-actions.ts";
21
+ import { statusGlyph } from "../../interface/format.ts";
22
+ import type { ClosedReason, ExecutionRecord, ExecutionStatus } from "../types.ts";
23
+
24
+ // ============================================================
25
+ // ClosedReason 枚举完整性
26
+ // ============================================================
27
+
28
+ describe("ClosedReason enum", () => {
29
+ it("包含全部 6 个值", () => {
30
+ const reasons: ClosedReason[] = [
31
+ "parent-shutdown",
32
+ "parent-fork",
33
+ "parent-new",
34
+ "user-close",
35
+ "cancelled",
36
+ "gc",
37
+ ];
38
+ // 类型检查:赋值不报错即通过
39
+ expect(reasons).toHaveLength(6);
40
+ });
41
+
42
+ it("ClosedReason 值类型正确", () => {
43
+ const r: ClosedReason = "gc";
44
+ expect(typeof r).toBe("string");
45
+ });
46
+ });
47
+
48
+ // ============================================================
49
+ // ExecutionStatus 不含旧字面量
50
+ // ============================================================
51
+
52
+ describe("ExecutionStatus type", () => {
53
+ it("合法值只有 running/closed(v4 B-1 两态收敛)", () => {
54
+ const validStatuses: ExecutionStatus[] = ["running", "closed"];
55
+ expect(validStatuses).toHaveLength(2);
56
+ });
57
+
58
+ it("done/failed/crashed 不是合法 ExecutionStatus", () => {
59
+ // 编译期验证:以下赋值应报 TS 类型错误(通过 @ts-expect-error 验证)
60
+ // @ts-expect-error SP-1: done 已移除
61
+ const _done: ExecutionStatus = "done";
62
+ // @ts-expect-error SP-1: failed 已移除
63
+ const _failed: ExecutionStatus = "failed";
64
+ // @ts-expect-error SP-1: crashed 已移除
65
+ const _crashed: ExecutionStatus = "crashed";
66
+ // 如果编译通过说明类型未正确收窄——此测试不应到达运行时
67
+ expect(_done).toBe("done"); // never reached if tsc errors
68
+ });
69
+ });
70
+
71
+ // ============================================================
72
+ // tryTransition + closedReason
73
+ // ============================================================
74
+
75
+ describe("tryTransition with closed + closedReason", () => {
76
+ function makeRunningRecord(): ExecutionRecord {
77
+ return createRecord("test-1", {
78
+ agent: "test-agent",
79
+ model: "test-model",
80
+ mode: "background",
81
+ task: "test task",
82
+ slug: "test-slug",
83
+ startedAt: Date.now(),
84
+ });
85
+ }
86
+
87
+ it("running → closed + gc 写入 closedReason", () => {
88
+ const record = makeRunningRecord();
89
+ expect(record.status).toBe("running");
90
+
91
+ const ok = tryTransition(record, "closed", "gc");
92
+ expect(ok).toBe(true);
93
+ expect(record.status).toBe("closed");
94
+ expect(record.closedReason).toBe("gc");
95
+ });
96
+
97
+ it("running → closed + user-close 写入 closedReason", () => {
98
+ const record = makeRunningRecord();
99
+ const ok = tryTransition(record, "closed", "user-close");
100
+ expect(ok).toBe(true);
101
+ expect(record.status).toBe("closed");
102
+ expect(record.closedReason).toBe("user-close");
103
+ });
104
+
105
+ it("running → closed 无 closedReason 默认 gc", () => {
106
+ const record = makeRunningRecord();
107
+ const ok = tryTransition(record, "closed");
108
+ expect(ok).toBe(true);
109
+ expect(record.status).toBe("closed");
110
+ expect(record.closedReason).toBe("gc");
111
+ });
112
+
113
+ it("running → closed + cancelled 写入 closedReason(cancelled 折入 closed)", () => {
114
+ const record = makeRunningRecord();
115
+ const ok = tryTransition(record, "closed", "cancelled");
116
+ expect(ok).toBe(true);
117
+ expect(record.status).toBe("closed");
118
+ expect(record.closedReason).toBe("cancelled");
119
+ });
120
+
121
+ it("closed 不可再转(CAS 互斥锁)", () => {
122
+ const record = makeRunningRecord();
123
+ tryTransition(record, "closed", "gc");
124
+ const ok = tryTransition(record, "closed", "user-close");
125
+ expect(ok).toBe(false);
126
+ expect(record.status).toBe("closed");
127
+ expect(record.closedReason).toBe("gc"); // 未被覆盖
128
+ });
129
+
130
+ it("closed + cancelled 不可再转(CAS 互斥锁,closedReason 不可覆盖)", () => {
131
+ const record = makeRunningRecord();
132
+ tryTransition(record, "closed", "cancelled");
133
+ const ok = tryTransition(record, "closed", "gc");
134
+ expect(ok).toBe(false);
135
+ expect(record.status).toBe("closed");
136
+ expect(record.closedReason).toBe("cancelled"); // 未被覆盖
137
+ });
138
+ });
139
+
140
+ // ============================================================
141
+ // completeRecord + closedReason
142
+ // ============================================================
143
+
144
+ describe("completeRecord with closed + closedReason", () => {
145
+ function makeRunningRecord(): ExecutionRecord {
146
+ return createRecord("test-cr", {
147
+ agent: "test-agent",
148
+ model: "test-model",
149
+ mode: "background",
150
+ task: "test task",
151
+ slug: "test-slug",
152
+ startedAt: Date.now(),
153
+ });
154
+ }
155
+
156
+ it("closed + gc 写入 closedReason + endedAt", () => {
157
+ const record = makeRunningRecord();
158
+ const result = { text: "done", turns: 1, durationMs: 100, success: true, sessionId: "s", toolCalls: [] };
159
+ completeRecord(record, result, "closed", "gc");
160
+
161
+ expect(record.status).toBe("closed");
162
+ expect(record.closedReason).toBe("gc");
163
+ expect(record.endedAt).toBeDefined();
164
+ expect(record.result).toBe("done");
165
+ });
166
+
167
+ it("closed + user-close 写入 closedReason", () => {
168
+ const record = makeRunningRecord();
169
+ const result = { text: "", turns: 0, durationMs: 0, success: true, sessionId: "s", toolCalls: [] };
170
+ completeRecord(record, result, "closed", "user-close");
171
+
172
+ expect(record.status).toBe("closed");
173
+ expect(record.closedReason).toBe("user-close");
174
+ });
175
+
176
+ it("closed 无 closedReason 默认 gc", () => {
177
+ const record = makeRunningRecord();
178
+ const result = { text: "", turns: 0, durationMs: 0, success: true, sessionId: "s", toolCalls: [] };
179
+ completeRecord(record, result, "closed");
180
+
181
+ expect(record.closedReason).toBe("gc");
182
+ });
183
+
184
+ it("closed + cancelled 写入 closedReason(cancelled 折入 closed)", () => {
185
+ const record = makeRunningRecord();
186
+ const result = { text: "", turns: 0, durationMs: 0, success: false, error: "cancelled", sessionId: "s", toolCalls: [] };
187
+ completeRecord(record, result, "closed", "cancelled");
188
+
189
+ expect(record.status).toBe("closed");
190
+ expect(record.closedReason).toBe("cancelled");
191
+ });
192
+ });
193
+
194
+ // ============================================================
195
+ // Notifier dedupKey: round 参与去重(MF-1 修复)
196
+ // ============================================================
197
+
198
+ describe("BgNotifier dedupKey", () => {
199
+ function createMockHost(hasRunning = false): NotifierHost {
200
+ return {
201
+ sendMessage: () => {},
202
+ hasRunningBackground: () => hasRunning,
203
+ };
204
+ }
205
+
206
+ it("同 id 不同 round 不被去重(round 参与 dedup key)", () => {
207
+ const host = createMockHost(false);
208
+ const notifier = new BgNotifier(host);
209
+ const sent: unknown[] = [];
210
+ // 重写 sendMessage 捕获
211
+ (host as { sendMessage: NotifierHost["sendMessage"] }).sendMessage = (msg) => { sent.push(msg); };
212
+
213
+ const record1: BgNotifyRecord = {
214
+ id: "sa-123",
215
+ status: "closed",
216
+ agent: "test",
217
+ result: "round 1",
218
+ startedAt: Date.now(),
219
+ endedAt: Date.now(),
220
+ round: 1,
221
+ };
222
+ const record2: BgNotifyRecord = {
223
+ id: "sa-123", // 同 id
224
+ status: "closed",
225
+ agent: "test",
226
+ result: "round 2",
227
+ startedAt: Date.now(),
228
+ endedAt: Date.now(),
229
+ round: 2, // 不同 round
230
+ };
231
+
232
+ notifier.notify(record1);
233
+ notifier.notify(record2);
234
+
235
+ // MF-1 修复:dedup key=id:round,不同 round 不互相吞
236
+ expect(sent).toHaveLength(2);
237
+ });
238
+
239
+ it("不同 id 不被去重", () => {
240
+ const host = createMockHost(false);
241
+ const notifier = new BgNotifier(host);
242
+ const sent: unknown[] = [];
243
+ (host as { sendMessage: NotifierHost["sendMessage"] }).sendMessage = (msg) => { sent.push(msg); };
244
+
245
+ const record1: BgNotifyRecord = {
246
+ id: "sa-111",
247
+ status: "closed",
248
+ agent: "test",
249
+ result: "result 1",
250
+ startedAt: Date.now(),
251
+ endedAt: Date.now(),
252
+ };
253
+ const record2: BgNotifyRecord = {
254
+ id: "sa-222",
255
+ status: "closed",
256
+ agent: "test",
257
+ result: "result 2",
258
+ startedAt: Date.now(),
259
+ endedAt: Date.now(),
260
+ };
261
+
262
+ notifier.notify(record1);
263
+ notifier.notify(record2);
264
+
265
+ expect(sent).toHaveLength(2);
266
+ });
267
+
268
+ it("closed status 被正确入队", () => {
269
+ const host = createMockHost(false);
270
+ const notifier = new BgNotifier(host);
271
+ const sent: unknown[] = [];
272
+ (host as { sendMessage: NotifierHost["sendMessage"] }).sendMessage = (msg) => { sent.push(msg); };
273
+
274
+ const record: BgNotifyRecord = {
275
+ id: "sa-333",
276
+ status: "closed",
277
+ closedReason: "gc",
278
+ agent: "test",
279
+ result: "some result",
280
+ startedAt: Date.now(),
281
+ endedAt: Date.now(),
282
+ };
283
+
284
+ notifier.notify(record);
285
+ expect(sent).toHaveLength(1);
286
+ });
287
+ });
288
+
289
+ // ============================================================
290
+ // mapExternalState: closed → ended
291
+ // ============================================================
292
+
293
+ describe("mapExternalState", () => {
294
+ it("running → active", () => {
295
+ expect(mapExternalState("running")).toBe("active");
296
+ });
297
+
298
+ it("closed → ended", () => {
299
+ expect(mapExternalState("closed")).toBe("ended");
300
+ });
301
+ });
302
+
303
+ // ============================================================
304
+ // statusGlyph: closed → ✓ success
305
+ // ============================================================
306
+
307
+ describe("statusGlyph", () => {
308
+ it("closed → ✓ success", () => {
309
+ const glyph = statusGlyph("closed");
310
+ expect(glyph.icon).toBe("✓");
311
+ expect(glyph.color).toBe("success");
312
+ });
313
+
314
+ it("running → accent (no icon)", () => {
315
+ const glyph = statusGlyph("running");
316
+ expect(glyph.icon).toBeUndefined();
317
+ expect(glyph.color).toBe("accent");
318
+ });
319
+ });
320
+
321
+ // ============================================================
322
+ // BgNotifyRecord.status 不含 done/failed
323
+ // ============================================================
324
+
325
+ describe("BgNotifyRecord.status", () => {
326
+ it("合法值只有 running/closed(v4 B-1 两态收敛)", () => {
327
+ // 编译期验证
328
+ const valid: BgNotifyRecord["status"][] = ["running", "closed"];
329
+ expect(valid).toHaveLength(2);
330
+ });
331
+
332
+ it("closed + closedReason 可构造", () => {
333
+ const record: BgNotifyRecord = {
334
+ id: "test",
335
+ status: "closed",
336
+ closedReason: "user-close",
337
+ agent: "test-agent",
338
+ result: "done",
339
+ startedAt: Date.now(),
340
+ endedAt: Date.now(),
341
+ };
342
+ expect(record.status).toBe("closed");
343
+ expect(record.closedReason).toBe("user-close");
344
+ });
345
+ });
@@ -288,6 +288,37 @@ describe("sendPromptCommand", () => {
288
288
  const child = { stdin: null } as unknown as ChildProcess;
289
289
  expect(() => sendPromptCommand(child, "task")).not.toThrow();
290
290
  });
291
+
292
+ // [V2 决策 3] streamingBehavior 可选参数:chatMode 统一投递热路径用 prompt+streamingBehavior
293
+ it("options.streamingBehavior='followUp' → 写入 streamingBehavior:followUp 字段", () => {
294
+ const child = makeChild();
295
+ sendPromptCommand(child, "queue this", { streamingBehavior: "followUp" });
296
+
297
+ const lines = readStdinLines(child);
298
+ expect(lines).toHaveLength(1);
299
+ const cmd = lines[0] as { type: string; message: string; streamingBehavior?: string };
300
+ expect(cmd.type).toBe("prompt");
301
+ expect(cmd.message).toBe("queue this");
302
+ expect(cmd.streamingBehavior).toBe("followUp");
303
+ });
304
+
305
+ it("options.streamingBehavior='steer' → 写入 streamingBehavior:steer 字段", () => {
306
+ const child = makeChild();
307
+ sendPromptCommand(child, "interrupt now", { streamingBehavior: "steer" });
308
+
309
+ const lines = readStdinLines(child);
310
+ const cmd = lines[0] as { streamingBehavior?: string };
311
+ expect(cmd.streamingBehavior).toBe("steer");
312
+ });
313
+
314
+ it("省略 options → 不写入 streamingBehavior 字段(向后兼容,首帧 prompt / 非 chatMode 调用方)", () => {
315
+ const child = makeChild();
316
+ sendPromptCommand(child, "first task");
317
+
318
+ const lines = readStdinLines(child);
319
+ const cmd = lines[0] as { streamingBehavior?: string };
320
+ expect(cmd.streamingBehavior).toBeUndefined();
321
+ });
291
322
  });
292
323
 
293
324
  // ============================================================
@@ -364,3 +395,69 @@ describe("writeStdinLine 背压 — write 返回 false 时 warn 不 throw", () =
364
395
  expect(warnSpy).not.toHaveBeenCalled();
365
396
  });
366
397
  });
398
+
399
+ // [review 修复 round2] 已删除 sendFollowUpCommand / sendSteerCommand 两 describe——
400
+ // 随 deliverToRunning(S2)死亡后两函数成零生产调用方的死 export,一并移除。
401
+ // busy 投递语义由 prompt + streamingBehavior("followUp"/"steer",sendPromptCommand)承担。
402
+
403
+ // ============================================================
404
+ // writeStdinLine EPIPE / ERR_STREAM_DESTROYED 检测(R3)
405
+ // ============================================================
406
+
407
+ describe("writeStdinLine EPIPE 检测 — EPIPE / ERR_STREAM_DESTROYED throw", () => {
408
+ it("child.stdin.write 抛 {code:'EPIPE'} → throw 含 EPIPE 关键词", () => {
409
+ const fakeStdin = {
410
+ write: vi.fn(() => {
411
+ const err = Object.assign(new Error("write after end"), { code: "EPIPE" });
412
+ throw err;
413
+ }),
414
+ destroyed: false,
415
+ } as unknown as PassThrough;
416
+ const child = { stdin: fakeStdin } as unknown as ChildProcess;
417
+
418
+ expect(() => respond(child, "req-epipe", { value: "x" })).toThrow(/EPIPE/);
419
+ });
420
+
421
+ it("child.stdin.write 抛 {code:'ERR_STREAM_DESTROYED'} → throw 含 EPIPE 关键词", () => {
422
+ const fakeStdin = {
423
+ write: vi.fn(() => {
424
+ const err = Object.assign(new Error("Stream is destroyed"), {
425
+ code: "ERR_STREAM_DESTROYED",
426
+ });
427
+ throw err;
428
+ }),
429
+ destroyed: false,
430
+ } as unknown as PassThrough;
431
+ const child = { stdin: fakeStdin } as unknown as ChildProcess;
432
+
433
+ expect(() => sendPromptCommand(child, "task")).toThrow(/EPIPE/);
434
+ });
435
+
436
+ it("child.stdin.destroyed=true → 静默跳过(guard 生效,不触发 write)", () => {
437
+ const fakeStdin = {
438
+ write: vi.fn(),
439
+ destroyed: true,
440
+ } as unknown as PassThrough;
441
+ const child = { stdin: fakeStdin } as unknown as ChildProcess;
442
+
443
+ expect(() => respond(child, "req-destroyed", { value: "x" })).not.toThrow();
444
+ // destroyed guard 生效,write 不被调用
445
+ expect(fakeStdin.write).not.toHaveBeenCalled();
446
+ });
447
+
448
+ it("child.stdin.write 抛非 EPIPE 错误 → warn 降级不 throw(兜底)", () => {
449
+ const fakeStdin = {
450
+ write: vi.fn(() => {
451
+ throw new Error("some other error");
452
+ }),
453
+ destroyed: false,
454
+ } as unknown as PassThrough;
455
+ const child = { stdin: fakeStdin } as unknown as ChildProcess;
456
+
457
+ // 非 EPIPE 错误应 warn 降级,不 throw
458
+ expect(() => respond(child, "req-other-err", { value: "x" })).not.toThrow();
459
+ expect(warnSpy).toHaveBeenCalledTimes(1);
460
+ const warnMsg = warnSpy.mock.calls[0]?.[0] as string;
461
+ expect(warnMsg).toContain("unexpected stdin write error");
462
+ });
463
+ });