@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
@@ -102,7 +102,7 @@ describe("reconstructFromFile", () => {
102
102
  expect(rec!.agent).toBe("worker");
103
103
  expect(rec!.mode).toBe("background");
104
104
  expect(rec!.task).toBe("do it");
105
- expect(rec!.status).toBe("done");
105
+ expect(rec!.status).toBe("closed");
106
106
  expect(rec!.turns).toHaveLength(1);
107
107
  expect(rec!.turns[0].text).toBe("hello world");
108
108
  expect(rec!.turnCount).toBe(1);
@@ -284,7 +284,7 @@ describe("reconstructFromFile", () => {
284
284
  const rec = reconstructFromFile(filePath);
285
285
  expect(rec!.lastError).toBe("API timeout");
286
286
  expect(rec!.error).toBe("API timeout");
287
- expect(rec!.status).toBe("failed"); // error stopReason → failed
287
+ expect(rec!.status).toBe("closed"); // error stopReason → failed
288
288
  });
289
289
 
290
290
  it("stopReason=aborted 无 errorMessage → lastError = 'aborted'", () => {
@@ -295,7 +295,7 @@ describe("reconstructFromFile", () => {
295
295
  ]);
296
296
  const rec = reconstructFromFile(filePath);
297
297
  expect(rec!.lastError).toBe("aborted");
298
- expect(rec!.status).toBe("failed");
298
+ expect(rec!.status).toBe("closed");
299
299
  });
300
300
 
301
301
  it("前序 error 但最后 stop → lastError 清除(镜像 turn_end 语义),status=done", () => {
@@ -309,7 +309,7 @@ describe("reconstructFromFile", () => {
309
309
  ]);
310
310
  const rec = reconstructFromFile(filePath);
311
311
  expect(rec!.lastError).toBeUndefined(); // 后续 stop 清除了 error
312
- expect(rec!.status).toBe("done");
312
+ expect(rec!.status).toBe("closed");
313
313
  expect(rec!.result).toBe("oops\n\nrecovered");
314
314
  });
315
315
  });
@@ -0,0 +1,178 @@
1
+ // src/execution/__tests__/session-runner-epipe.test.ts
2
+ //
3
+ // [v4 A-1] session-runner 异步 stdin 'error' listener 测试。
4
+ //
5
+ // 背景:writeStdinLine 的 try/catch 只覆盖同步 write 抛错;子进程退出后内核回写 EPIPE
6
+ // 以异步 stream 'error' event 到达,若无 listener 会让 Node 抛 unhandled 'error' 崩主进程(P1)。
7
+ // runSpawn 在 spawn 后、首次 stdin 写入前注册 child.stdin.on('error') handler,合并
8
+ // 同步/异步 EPIPE 计数(共用 stdin-writer 的 recordEpipeFailure)。
9
+ //
10
+ // 验收:① listener 存在 ② error 触发后 spawnedChildren 不含该 recordId
11
+ // ③ recordEpipeFailure 计数累加(同步/异步合并)
12
+
13
+ import { spawn } from "node:child_process";
14
+
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+
17
+ vi.mock("node:child_process", async () => {
18
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
19
+ return {
20
+ spawn: vi.fn(() => new FakeChild()),
21
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
22
+ execFile: vi.fn(
23
+ (
24
+ _cmd: string,
25
+ _args: readonly string[],
26
+ _opts: unknown,
27
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
28
+ ) => cb(new Error("execFile not configured in this test")),
29
+ ),
30
+ };
31
+ });
32
+
33
+ vi.mock("node:fs", async () => {
34
+ const actual = await import("node:fs");
35
+ return {
36
+ default: {
37
+ ...actual,
38
+ mkdirSync: vi.fn(),
39
+ existsSync: vi.fn(() => false),
40
+ appendFileSync: vi.fn(),
41
+ writeFileSync: vi.fn(),
42
+ readdirSync: vi.fn(() => []),
43
+ },
44
+ mkdirSync: vi.fn(),
45
+ existsSync: vi.fn(() => false),
46
+ appendFileSync: vi.fn(),
47
+ writeFileSync: vi.fn(),
48
+ readdirSync: vi.fn(() => []),
49
+ promises: actual.promises,
50
+ };
51
+ });
52
+
53
+ vi.mock("../alive-store.ts", () => ({ writeAliveMarker: vi.fn() }));
54
+
55
+ vi.mock("../session-pending.ts", () => ({
56
+ readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
57
+ }));
58
+
59
+ vi.mock("../temp-prompt.ts", () => ({
60
+ writePromptToTempFile: vi.fn(async (agent: string) => {
61
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
62
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
63
+ }),
64
+ cleanupTempPrompt: vi.fn(async () => {}),
65
+ }));
66
+
67
+ import {
68
+ EPIPE_FAILURE_THRESHOLD,
69
+ recordEpipeFailure,
70
+ resetAllEpipeFailures,
71
+ } from "../stdin-writer.ts";
72
+ import { runSpawn, spawnedChildren } from "../session-runner.ts";
73
+ import { _resetLifecycleState } from "../lifecycle-manager.ts";
74
+ import {
75
+ FakeChild,
76
+ lastSpawnedChild as lastSpawnedChildOf,
77
+ makeCtx,
78
+ makeOpts,
79
+ makeRecord,
80
+ waitForSpawn as waitForSpawnOf,
81
+ } from "./helpers/spawn-mock.ts";
82
+
83
+ const mockSpawn = vi.mocked(spawn);
84
+ const lastSpawnedChild = (): FakeChild => lastSpawnedChildOf(mockSpawn);
85
+ const waitForSpawn = (timeoutMs = 1000): Promise<void> => waitForSpawnOf(mockSpawn, timeoutMs);
86
+
87
+ /** 构造 EPIPE errno 错误(模拟子进程退出后内核回写 EPIPE)。 */
88
+ function epipeError(message = "write after end"): NodeJS.ErrnoException {
89
+ const err = new Error(message) as NodeJS.ErrnoException;
90
+ err.code = "EPIPE";
91
+ return err;
92
+ }
93
+
94
+ /** 手动 emit close 让 runSpawn 的 exitCode promise resolve(非 chatMode 默认 close resolve)。 */
95
+ function settleRunSpawn(child: FakeChild): void {
96
+ child.stdout.end();
97
+ child.stderr.end();
98
+ child.emit("close", 0);
99
+ }
100
+
101
+ describe("[v4 A-1] session-runner 异步 stdin 'error' listener", () => {
102
+ beforeEach(() => {
103
+ vi.clearAllMocks();
104
+ _resetLifecycleState();
105
+ resetAllEpipeFailures();
106
+ spawnedChildren.clear();
107
+ });
108
+
109
+ afterEach(() => {
110
+ _resetLifecycleState();
111
+ resetAllEpipeFailures();
112
+ spawnedChildren.clear();
113
+ });
114
+
115
+ it("① spawn 后注册 child.stdin 'error' listener", async () => {
116
+ const record = makeRecord("sa-listener");
117
+ const promise = runSpawn(record, "Task: listener-exists", makeOpts(), makeCtx());
118
+
119
+ await waitForSpawn();
120
+ const child = lastSpawnedChild();
121
+
122
+ // listener 已注册——无 listener 时异步 EPIPE 会崩主进程(这是 A-1 修复的 P1)
123
+ expect(child.stdin.listenerCount("error")).toBeGreaterThan(0);
124
+
125
+ settleRunSpawn(child);
126
+ await promise;
127
+ });
128
+
129
+ it("②③ async stdin 'error' → 移出 spawnedChildren + recordEpipeFailure 计数递增", async () => {
130
+ const record = makeRecord("sa-async-epipe");
131
+ const promise = runSpawn(record, "Task: async-epipe", makeOpts(), makeCtx());
132
+
133
+ await waitForSpawn();
134
+ const child = lastSpawnedChild();
135
+
136
+ // runSpawn 已把 child 注册进 spawnedChildren
137
+ expect(spawnedChildren.has(record.id)).toBe(true);
138
+
139
+ // 发一次异步 stdin 'error'(count 0→1,未达阈值,handler 只 warn 不 throw)
140
+ child.stdin.emit("error", epipeError());
141
+
142
+ // ② spawnedChildren 已移除该 recordId(标记 dead)
143
+ expect(spawnedChildren.has(record.id)).toBe(false);
144
+
145
+ // ③ 计数累加:handler 调 recordEpipeFailure 设为 1;此处再调一次应返回 2
146
+ // (若 handler 未运行,counter 仍为 0,本次调用只会返回 1)
147
+ expect(recordEpipeFailure(record.id)).toBe(2);
148
+
149
+ settleRunSpawn(child);
150
+ await promise;
151
+ });
152
+
153
+ it("③ 同步预置计数 + 异步 stdin 'error' 合并计数(handler 不 throw,防 listener 内 throw 崩进程)", async () => {
154
+ const record = makeRecord("sa-threshold");
155
+ const promise = runSpawn(record, "Task: threshold", makeOpts(), makeCtx());
156
+
157
+ await waitForSpawn();
158
+ const child = lastSpawnedChild();
159
+
160
+ // 预置 count=1(模拟 deliverMessage 同步路径已计 1 次失败——证明共用同一计数器)
161
+ expect(recordEpipeFailure(record.id)).toBe(1);
162
+
163
+ // [v4 A-1 裁决] async handler 内不 throw(stream 'error' listener 内 throw 会经 emit
164
+ // 传播为 uncaughtException 崩主进程,违背 A-1 防崩)——达阈值 throw 留同步路径。
165
+ // emit 不抛错:
166
+ expect(() => child.stdin.emit("error", epipeError())).not.toThrow();
167
+
168
+ // handler 调 recordEpipeFailure → count=2(达阈值但 async 不 throw);此处再调返回 3
169
+ expect(recordEpipeFailure(record.id)).toBe(3);
170
+ expect(EPIPE_FAILURE_THRESHOLD).toBe(2);
171
+
172
+ // handler 已移出 spawnedChildren(标记 dead,下次 deliverMessage 检测走冷路径)
173
+ expect(spawnedChildren.has(record.id)).toBe(false);
174
+
175
+ settleRunSpawn(child);
176
+ await promise;
177
+ });
178
+ });
@@ -28,6 +28,7 @@ vi.mock("node:child_process", async () => {
28
28
  class FakeChild extends EventEmitter {
29
29
  pid = 12345;
30
30
  stdout = new PassThrough();
31
+ stdin = new PassThrough(); // runSpawn 注册 stdin EPIPE handler(session-runner),FakeChild 必须提供
31
32
  stderr = new PassThrough();
32
33
  killed = false;
33
34
  killSignal: string | undefined;
@@ -40,7 +41,15 @@ vi.mock("node:child_process", async () => {
40
41
 
41
42
  return {
42
43
  spawn: vi.fn(() => new FakeChild()),
43
- execFileSync: vi.fn(() => ""),
44
+ // buildEnvBlock execFile 异步取 git branch:默认 err-first 兜底(catch → branch=""
45
+ execFile: vi.fn(
46
+ (
47
+ _cmd: string,
48
+ _args: readonly string[],
49
+ _opts: unknown,
50
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
51
+ ) => cb(new Error("execFile not configured in this test")),
52
+ ),
44
53
  };
45
54
  });
46
55
 
@@ -76,9 +85,10 @@ vi.mock("../temp-prompt.ts", () => ({
76
85
  cleanupTempPrompt: vi.fn(async () => {}),
77
86
  }));
78
87
 
79
- import { execFileSync, spawn } from "node:child_process";
88
+ import { spawn } from "node:child_process";
80
89
  import * as fs from "node:fs";
81
90
 
91
+ import { waitForSpawn } from "./helpers/spawn-mock.ts";
82
92
  import { createRecord } from "../execution-record.ts";
83
93
  import {
84
94
  applySchemaEnvToChildEnv,
@@ -88,7 +98,6 @@ import {
88
98
  } from "../session-runner.ts";
89
99
 
90
100
  const mockSpawn = vi.mocked(spawn);
91
- const mockExec = vi.mocked(execFileSync);
92
101
  const mockExistsSync = vi.mocked(fs.existsSync);
93
102
 
94
103
  interface FakeChild {
@@ -111,20 +120,6 @@ function getLastSpawnEnv(): Record<string, string | undefined> {
111
120
  return mockSpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string | undefined> ?? {};
112
121
  }
113
122
 
114
- /**
115
- * 等待 runSpawn 内部调到 spawn。
116
- * runSpawn 是 async,spawn 在 writePromptToTempFile await 之后才调。
117
- */
118
- async function waitForSpawn(timeoutMs = 1000): Promise<void> {
119
- const start = Date.now();
120
- while (mockSpawn.mock.results.length === 0) {
121
- if (Date.now() - start > timeoutMs) {
122
- throw new Error(`spawn was not called within ${timeoutMs}ms`);
123
- }
124
- await new Promise((r) => setTimeout(r, 5));
125
- }
126
- }
127
-
128
123
  // ── 公共 fixture ──
129
124
 
130
125
  function makeRecord() {
@@ -238,7 +233,6 @@ describe("runSpawn schemaEnv childEnv 注入 (T3.9/T3.11)", () => {
238
233
  beforeEach(() => {
239
234
  vi.clearAllMocks();
240
235
  mockExistsSync.mockReturnValue(false);
241
- mockExec.mockReturnValue("");
242
236
  });
243
237
 
244
238
  afterEach(() => {
@@ -254,7 +248,7 @@ describe("runSpawn schemaEnv childEnv 注入 (T3.9/T3.11)", () => {
254
248
  const ctx = makeCtx();
255
249
 
256
250
  const resultPromise = runSpawn(record, "test task", opts, ctx);
257
- await waitForSpawn();
251
+ await waitForSpawn(mockSpawn);
258
252
  const childEnv = getLastSpawnEnv();
259
253
  // BC-6: schemaEnv 未传入 → PI_WORKFLOW_SCHEMA 应为 process.env 原值(我们的代码不注入)
260
254
  expect(childEnv.PI_WORKFLOW_SCHEMA).toBe(process.env.PI_WORKFLOW_SCHEMA);
@@ -272,7 +266,7 @@ describe("runSpawn schemaEnv childEnv 注入 (T3.9/T3.11)", () => {
272
266
  const ctx = makeCtx();
273
267
 
274
268
  const resultPromise = runSpawn(record, "test task", opts, ctx);
275
- await waitForSpawn();
269
+ await waitForSpawn(mockSpawn);
276
270
  const childEnv = getLastSpawnEnv();
277
271
  expect(childEnv.PI_WORKFLOW_SCHEMA).toBe(schemaJson);
278
272
 
@@ -303,7 +297,7 @@ describe("runSpawn schemaEnv childEnv 注入 (T3.9/T3.11)", () => {
303
297
  const ctx = makeCtx();
304
298
 
305
299
  const resultPromise = runSpawn(record, "test task", opts, ctx);
306
- await waitForSpawn();
300
+ await waitForSpawn(mockSpawn);
307
301
  const childEnv = getLastSpawnEnv();
308
302
  // fork depth env 应存在
309
303
  expect(childEnv.PI_SUBAGENT_FORK_DEPTH).toBe("1");
@@ -82,6 +82,8 @@ vi.mock("../subagent-service.ts", () => ({
82
82
  initSession = mockInitSession;
83
83
  // W3: index.ts session_start 注入 UI handler 时调用
84
84
  setUiRequestHandler = mockSetUiRequestHandler;
85
+ startGcTimer = vi.fn();
86
+ recoverManifestTmpFiles = vi.fn(async () => ({ deleted: 0, recovered: 0 }));
85
87
  constructor(init: unknown) {
86
88
  capturedConstructorArg.current = init;
87
89
  }
@@ -167,14 +169,14 @@ describe("session_start worktree reaper", () => {
167
169
  vi.clearAllMocks();
168
170
  });
169
171
 
170
- it("session_start 触发 WTM.scan 调用", () => {
172
+ it("session_start 触发 WTM.scan 调用", async () => {
171
173
  const { pi, getSessionStartHandler } = createMockPi();
172
174
  subagentsExtension(pi);
173
175
 
174
176
  const handler = getSessionStartHandler();
175
177
  expect(handler).toBeDefined();
176
178
 
177
- handler!(
179
+ await handler!(
178
180
  { type: "session_start", reason: "startup" },
179
181
  createMockCtx(),
180
182
  );
@@ -184,7 +186,7 @@ describe("session_start worktree reaper", () => {
184
186
  expect(mockScan).toHaveBeenCalledWith();
185
187
  });
186
188
 
187
- it("scan 抛错不阻断 session_start", () => {
189
+ it("scan 抛错不阻断 session_start", async () => {
188
190
  mockScan.mockImplementation(() => {
189
191
  throw new Error("git not found");
190
192
  });
@@ -196,23 +198,23 @@ describe("session_start worktree reaper", () => {
196
198
  expect(handler).toBeDefined();
197
199
 
198
200
  // 不应抛错
199
- expect(() => {
201
+ await expect(
200
202
  handler!(
201
203
  { type: "session_start", reason: "startup" },
202
204
  createMockCtx(),
203
- );
204
- }).not.toThrow();
205
+ )
206
+ ).resolves.toBeUndefined();
205
207
 
206
208
  // service 仍然被注册(启动未被阻断)
207
209
  expect(mockSetSubagentService).toHaveBeenCalled();
208
210
  });
209
211
 
210
- it("mainSessionFile 被缓存并传给 SubagentService", () => {
212
+ it("mainSessionFile 被缓存并传给 SubagentService", async () => {
211
213
  const { pi, getSessionStartHandler } = createMockPi();
212
214
  subagentsExtension(pi);
213
215
 
214
216
  const handler = getSessionStartHandler();
215
- handler!(
217
+ await handler!(
216
218
  { type: "session_start", reason: "startup" },
217
219
  createMockCtx(),
218
220
  );