@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,215 @@
1
+ // src/execution/__tests__/chatmode-round-notify-real-chain.test.ts
2
+ //
3
+ // [N2] chatMode 轮次通知正文——真实 session-runner 链路测试。
4
+ //
5
+ // round2 审查实证的断链:agent_settled → onRoundSettled 先 notifyComplete(此时
6
+ // record.result 从未被写——成功轮次的 MF-2 原写点 doFinalizeRoundToIdle 对
7
+ // runAndFinalize early return 不可达)→ 轮次通知正文恒 "(empty)",turns[].text 已累积
8
+ // 但 notify 读的是 record.result。修复:onRoundSettled 在 notify 前从本轮 turns 派生
9
+ // 回复文本(对齐 collectResult 的 getFullText)写入 record.result。
10
+ //
11
+ // 与 chatmode-first-round-closure-service.test.ts(mock session-runner)不同,本文件用
12
+ // 真实 session-runner(FakeChild 驱动 text_delta / agent_settled)+ 真实
13
+ // service.execute(conversation:true),禁止手工预置 record.result——那正是掩盖断链的方式。
14
+ // mock 结构与 run-spawn-chatmode-settled.test.ts 一致(FakeChild + session-pending count=0)。
15
+
16
+ import { spawn } from "node:child_process";
17
+ import * as fs from "node:fs";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+
21
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
+
23
+ const { loggerMock } = vi.hoisted(() => ({
24
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
25
+ }));
26
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
27
+
28
+ vi.mock("node:child_process", async () => {
29
+ const { FakeChild } = await import("./helpers/spawn-mock.ts");
30
+ return {
31
+ spawn: vi.fn(() => new FakeChild()),
32
+ // buildEnvBlock 的 git branch 调用(execFile 异步):默认 err-first 兜底 → catch → branch=""
33
+ execFile: vi.fn(
34
+ (
35
+ _cmd: string,
36
+ _args: readonly string[],
37
+ _opts: unknown,
38
+ cb: (err: Error | null, stdout?: string, stderr?: string) => void,
39
+ ) => cb(new Error("execFile not configured in this test")),
40
+ ),
41
+ };
42
+ });
43
+
44
+ vi.mock("node:fs", async () => {
45
+ const actual = await import("node:fs");
46
+ return {
47
+ default: {
48
+ ...actual,
49
+ mkdirSync: vi.fn(),
50
+ existsSync: vi.fn(() => false),
51
+ appendFileSync: vi.fn(),
52
+ writeFileSync: vi.fn(),
53
+ readdirSync: vi.fn(() => []),
54
+ },
55
+ mkdirSync: vi.fn(),
56
+ existsSync: vi.fn(() => false),
57
+ appendFileSync: vi.fn(),
58
+ writeFileSync: vi.fn(),
59
+ readdirSync: vi.fn(() => []),
60
+ // tmp 目录创建/清理走真实实现(本文件用例级隔离需要)
61
+ mkdtempSync: actual.mkdtempSync,
62
+ rmSync: actual.rmSync,
63
+ promises: actual.promises,
64
+ };
65
+ });
66
+
67
+ // removeAliveMarker 一并 mock(finalize-record 消费;成功轮次 early return 不触达,防御性补全)
68
+ vi.mock("../alive-store.ts", () => ({
69
+ writeAliveMarker: vi.fn(),
70
+ removeAliveMarker: vi.fn(),
71
+ }));
72
+
73
+ // chatMode agent_end 早返回不读后代判定;统一 count=0(对齐 harness)。
74
+ vi.mock("../session-pending.ts", () => ({
75
+ readActivePendingFromSessionFile: vi.fn(() => ({ count: 0 })),
76
+ }));
77
+
78
+ vi.mock("../temp-prompt.ts", () => ({
79
+ writePromptToTempFile: vi.fn(async (agent: string) => {
80
+ const safeName = agent.replace(/[^\w.-]+/g, "_");
81
+ return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` };
82
+ }),
83
+ cleanupTempPrompt: vi.fn(async () => {}),
84
+ }));
85
+
86
+ import { _resetLifecycleState } from "../lifecycle-manager.ts";
87
+ import { ModelConfigService } from "../model-config-service.ts";
88
+ import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
89
+ import type { ExecutionRecord } from "../types.ts";
90
+ import { SubagentService } from "../subagent-service.ts";
91
+ import type { PiLike } from "../subagent-service.ts";
92
+ import {
93
+ emitStdoutLine,
94
+ lastSpawnedChild,
95
+ sessionHeader,
96
+ waitForSpawn,
97
+ } from "./helpers/spawn-mock.ts";
98
+
99
+ const mockSpawn = vi.mocked(spawn);
100
+
101
+ const STUB_MODEL: ModelInfo = { id: "test-model", name: "Test", provider: "test", reasoning: false };
102
+
103
+ /** 最小合法 registry(initModel fail-fast 需要;resolveModel 第三层直接透传 ctxModel)。 */
104
+ function makeEmptyRegistry(): ModelRegistryLike {
105
+ return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
106
+ }
107
+
108
+ function makePi(): PiLike & {
109
+ appendEntry: ReturnType<typeof vi.fn>;
110
+ events: { emit: ReturnType<typeof vi.fn> };
111
+ sendMessage: ReturnType<typeof vi.fn>;
112
+ } {
113
+ return { appendEntry: vi.fn(), events: { emit: vi.fn() }, sendMessage: vi.fn() };
114
+ }
115
+
116
+ /** 暴露私有 store 的接口(测试专用 cast)。 */
117
+ interface ServiceInternals {
118
+ store: { getMutable(id: string): ExecutionRecord | undefined };
119
+ }
120
+
121
+ describe("[N2] chatMode 轮次通知正文:真实 session-runner 链路", () => {
122
+ let agentDir: string;
123
+ let service: SubagentService;
124
+ let pi: ReturnType<typeof makePi>;
125
+ let internals: ServiceInternals;
126
+
127
+ beforeEach(() => {
128
+ vi.clearAllMocks();
129
+ // lifecycle-manager 模块级单例(idleTimers Map 跨用例共享),每用例前清空防泄漏。
130
+ _resetLifecycleState();
131
+ agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "round-notify-"));
132
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
133
+ modelService.initModel({
134
+ modelRegistry: makeEmptyRegistry(),
135
+ sessionId: "root-session",
136
+ ctxModel: STUB_MODEL,
137
+ });
138
+ service = new SubagentService({ cwd: agentDir, modelService });
139
+ pi = makePi();
140
+ service.initSession({ pi, sessionId: "root-session" });
141
+ internals = service as unknown as ServiceInternals;
142
+ });
143
+
144
+ afterEach(() => {
145
+ service.dispose();
146
+ _resetLifecycleState();
147
+ vi.restoreAllMocks();
148
+ fs.rmSync(agentDir, { recursive: true, force: true });
149
+ });
150
+
151
+ it("真实 execute(conversation:true) + FakeChild 驱动 text_delta/agent_settled → 通知正文含本轮真实回复(非 (empty))", async () => {
152
+ const ROUND_REPLY = "THE ROUND REPLY";
153
+
154
+ // 真实链路:execute → kickOffBackground → runAndFinalize → 真实 runSpawn → FakeChild。
155
+ // ctx.onRoundSettled 由 buildSessionRunnerContext 注入(真实回调,非 mock)。
156
+ const handle = await service.execute({
157
+ task: "tell me something",
158
+ slug: "round-notify",
159
+ conversation: true,
160
+ });
161
+
162
+ await waitForSpawn(mockSpawn);
163
+ const child = lastSpawnedChild(mockSpawn);
164
+
165
+ // 真实事件链:header(握手加速)→ text_delta(本轮回复累积进 turns)→ turn_end →
166
+ // agent_end(chatMode 保活)→ agent_settled(真空闲:armIdleTimer → onRoundSettled)。
167
+ emitStdoutLine(child, sessionHeader("sess-round-1"));
168
+ emitStdoutLine(child, {
169
+ type: "message_update",
170
+ // type 对齐 pi-ai AssistantMessageEvent 真实协议(text 增量 = "text_delta",带 contentIndex)。
171
+ // 曾用 {type:"text"} 假类型——旧实现不查 type 只看 delta 碰巧兼容;现实现按 type 正向
172
+ // 分流(toolcall_delta 不混入 text 流),假类型事件被正确丢弃,fake 必须对齐真实协议。
173
+ assistantMessageEvent: { type: "text_delta", contentIndex: 0, delta: ROUND_REPLY },
174
+ });
175
+ emitStdoutLine(child, { type: "turn_end" });
176
+ emitStdoutLine(child, { type: "agent_end", willRetry: false });
177
+ emitStdoutLine(child, { type: "agent_settled" });
178
+
179
+ // agent_settled → onRoundSettled(round+1 + record.result 派生 + notifyComplete)→
180
+ // 无其他 busy background → 立即 flush → pi.sendMessage。
181
+ await vi.waitFor(() => {
182
+ expect(pi.sendMessage).toHaveBeenCalledTimes(1);
183
+ });
184
+
185
+ const sentMsg = pi.sendMessage.mock.calls[0]![0] as { customType: string; content: string };
186
+ expect(sentMsg.customType).toBe("subagent-bg-notify");
187
+ expect(sentMsg.content).toContain("finished a round");
188
+ // [N2] 核心:正文含本轮真实回复文本(修复前此处是 "(empty)")
189
+ expect(sentMsg.content).toContain(ROUND_REPLY);
190
+ expect(sentMsg.content).not.toContain("(empty)");
191
+
192
+ // record 侧:turns 已累积 + result 从 turns 真实派生(非手工预置)+ running-resumable
193
+ const record = internals.store.getMutable(handle.subagentId);
194
+ expect(record).toBeDefined();
195
+ expect(record!.turns[0]!.text).toBe(ROUND_REPLY);
196
+ expect(record!.result).toBe(ROUND_REPLY);
197
+ expect(record!.status).toBe("running");
198
+ expect(record!.round).toBe(1);
199
+
200
+ // 收尾:close 让 runSpawn resolve → runAndFinalize 续体 early return → .then 的
201
+ // notifyComplete 被同 id:round dedup 吞——总发送数仍恰为 1。
202
+ child.stdout.end();
203
+ child.stderr.end();
204
+ child.emit("close", 0);
205
+ await new Promise((r) => setTimeout(r, 30));
206
+ expect(pi.sendMessage).toHaveBeenCalledTimes(1);
207
+
208
+ // [C2] close 现状语义扩展:终态化不再发新通知(总数仍 1),末条轮次通知含
209
+ // Full transcript 指针行——真实链路 sessionHeader 已回填 record.sessionFile
210
+ //(session-runner.ts:1039),chatMode:true 经 toNotifyRecord 条件透传到通知正文。
211
+ const lastMsg = pi.sendMessage.mock.calls[0]![0] as { content: string; details?: { sessionFile?: string } };
212
+ expect(lastMsg.details?.sessionFile).toBe(record!.sessionFile);
213
+ expect(lastMsg.content).toContain(`\n\nFull transcript: ${record!.sessionFile}`);
214
+ });
215
+ });
@@ -0,0 +1,198 @@
1
+ // src/execution/__tests__/conversation-wiring.test.ts
2
+ //
3
+ // [M9] conversation:true → record.chatMode 接线穿透测试。
4
+ //
5
+ // 背景:「持续对话」特性的 LLM 入口接线(subagent-tool params → startHandler 透传 →
6
+ // execute → createRecordForMode 的 `chatMode: opts.conversation === true` +
7
+ // `idleTimeoutMs: opts.idleTimeoutMs`,subagent-service.ts L1234-1235)此前无任何测试——
8
+ // 全部 chatMode 测试手工构造 chatMode:true record,startHandler 测试不传 conversation。
9
+ // 若参数名漂移或透传丢失(subagent-actions.ts L206-207),1970 个用例仍全绿。
10
+ //
11
+ // 两层验证:
12
+ // 1. 接线层:真实 SubagentService.execute(mock runSpawn pending 阻断 detached 收尾)
13
+ // → 断言内存 record.chatMode === true / idleTimeoutMs 生效;缺省对照 chatMode === false
14
+ // 2. 透传层:startHandler + mock service → 断言 execute 收到 conversation/idleTimeoutMs 原值
15
+
16
+ import * as fs from "node:fs";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+
20
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
21
+
22
+ const { loggerMock } = vi.hoisted(() => ({
23
+ loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
24
+ }));
25
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
26
+
27
+ // mock session-runner:runSpawn 返回永不 resolve 的 promise(阻断 runAndFinalize 收尾,
28
+ // record 停在 running,execute 返回后立即可断言 createRecordForMode 的接线产物)。
29
+ vi.mock("../session-runner.ts", () => ({
30
+ runSpawn: vi.fn(() => new Promise(() => {})),
31
+ killAllSpawnedChildren: vi.fn(),
32
+ getChildByRecord: vi.fn(() => undefined),
33
+ spawnedChildren: new Map(),
34
+ }));
35
+
36
+ import { runSpawn } from "../session-runner.ts";
37
+ import { startHandler } from "../../interface/subagent-actions.ts";
38
+ import { ModelConfigService } from "../model-config-service.ts";
39
+ import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts";
40
+ import { RecordStore } from "../record-store.ts";
41
+ import { SubagentService } from "../subagent-service.ts";
42
+ import type { ExecutionHandle, SubagentToolDetails } from "../types.ts";
43
+
44
+ const mockRunSpawn = vi.mocked(runSpawn);
45
+
46
+ const STUB_MODEL: ModelInfo = { id: "test-model", name: "Test", provider: "test", reasoning: false };
47
+
48
+ /** 最小合法 registry(initModel fail-fast 需要;resolveModel 第三层直接透传 ctxModel)。 */
49
+ function makeEmptyRegistry(): ModelRegistryLike {
50
+ return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true };
51
+ }
52
+
53
+ /** initSession 注入的最小 pi duck-type(同 subagent-service PiLike 形状,结构匹配即可)。 */
54
+ interface PiStub {
55
+ appendEntry(customType: string, data?: unknown): void;
56
+ events: { emit(channel: string, data: unknown): void };
57
+ sendMessage(
58
+ message: { customType: string; content: string; display: boolean; details?: unknown },
59
+ options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
60
+ ): void;
61
+ }
62
+
63
+ function makePi(): PiStub {
64
+ return {
65
+ appendEntry: vi.fn(),
66
+ events: { emit: vi.fn() },
67
+ sendMessage: vi.fn(),
68
+ };
69
+ }
70
+
71
+ /** 暴露私有 store 的接口(测试专用 cast)。 */
72
+ interface ServiceInternals {
73
+ store: RecordStore;
74
+ }
75
+
76
+ // ============================================================
77
+ // 1. 接线层:execute({conversation, idleTimeoutMs}) → record 字段
78
+ // ============================================================
79
+
80
+ describe("[M9] conversation:true 接线:execute → createRecordForMode", () => {
81
+ let agentDir: string;
82
+ let service: SubagentService;
83
+ let store: RecordStore;
84
+
85
+ beforeEach(() => {
86
+ agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "conv-wiring-"));
87
+ const modelService = new ModelConfigService({ agentDir, cwd: agentDir });
88
+ modelService.initModel({
89
+ modelRegistry: makeEmptyRegistry(),
90
+ sessionId: "root-session",
91
+ ctxModel: STUB_MODEL,
92
+ });
93
+ service = new SubagentService({ cwd: agentDir, modelService });
94
+ service.initSession({ pi: makePi(), sessionId: "root-session" });
95
+ store = (service as unknown as ServiceInternals).store;
96
+ mockRunSpawn.mockClear();
97
+ });
98
+
99
+ afterEach(() => {
100
+ service.dispose();
101
+ fs.rmSync(agentDir, { recursive: true, force: true });
102
+ });
103
+
104
+ it("conversation:true + idleTimeoutMs:12345 → record.chatMode===true、idleTimeoutMs 生效", async () => {
105
+ const handle = await service.execute({
106
+ task: "keep chatting with me",
107
+ slug: "conv-test",
108
+ conversation: true,
109
+ idleTimeoutMs: 12345,
110
+ });
111
+
112
+ const record = store.getMutable(handle.subagentId);
113
+ expect(record).toBeDefined();
114
+ expect(record!.chatMode).toBe(true);
115
+ expect(record!.idleTimeoutMs).toBe(12345);
116
+ expect(record!.status).toBe("running");
117
+ // execute 走到 kickOffBackground(runSpawn 已被调用)——完整接线而非 early return
118
+ expect(mockRunSpawn).toHaveBeenCalledTimes(1);
119
+ });
120
+
121
+ it("conversation 缺省 → record.chatMode===false、idleTimeoutMs undefined(一次性模式不误升级)", async () => {
122
+ const handle = await service.execute({
123
+ task: "one shot task",
124
+ slug: "oneshot-test",
125
+ });
126
+
127
+ const record = store.getMutable(handle.subagentId);
128
+ expect(record).toBeDefined();
129
+ expect(record!.chatMode).toBe(false);
130
+ expect(record!.idleTimeoutMs).toBeUndefined();
131
+ });
132
+ });
133
+
134
+ // ============================================================
135
+ // 2. 透传层:startHandler(service, {conversation, idleTimeoutMs}) → service.execute
136
+ // ============================================================
137
+
138
+ function makeHandle(subagentId: string): ExecutionHandle {
139
+ const details: SubagentToolDetails = {
140
+ status: "running",
141
+ mode: "background",
142
+ agent: "worker",
143
+ model: "test/model",
144
+ thinkingLevel: undefined,
145
+ slug: "conv-test",
146
+ turns: 0,
147
+ totalTokens: 0,
148
+ elapsedSeconds: 0,
149
+ eventLog: [],
150
+ displayItems: [],
151
+ result: undefined,
152
+ };
153
+ return { mode: "background", subagentId, sessionFile: undefined, details };
154
+ }
155
+
156
+ function makeService(): SubagentService & { execute: ReturnType<typeof vi.fn> } {
157
+ return {
158
+ execute: vi.fn(async () => makeHandle("sa-conv-1")),
159
+ findRecord: vi.fn(() => undefined),
160
+ cancel: vi.fn(() => false),
161
+ collectRecords: vi.fn(() => []),
162
+ getFullRecord: vi.fn(() => undefined),
163
+ } as unknown as SubagentService & { execute: ReturnType<typeof vi.fn> };
164
+ }
165
+
166
+ describe("[M9] startHandler 透传:conversation/idleTimeoutMs → execute 入参", () => {
167
+ it("conversation:true + idleTimeoutMs:12345 原值透传给 service.execute", async () => {
168
+ const svc = makeService();
169
+ const result = await startHandler(
170
+ svc,
171
+ { task: "chat task", slug: "conv-pass", conversation: true, idleTimeoutMs: 12345 },
172
+ undefined,
173
+ );
174
+
175
+ expect(svc.execute).toHaveBeenCalledTimes(1);
176
+ expect(svc.execute).toHaveBeenCalledWith(
177
+ expect.objectContaining({
178
+ task: "chat task",
179
+ slug: "conv-pass",
180
+ conversation: true,
181
+ idleTimeoutMs: 12345,
182
+ }),
183
+ );
184
+ // bg 响应回执(LLM 可见):detached + running
185
+ expect(result.kind).toBe("bg");
186
+ expect(result.subagentId).toBe("sa-conv-1");
187
+ expect(result.response.status).toBe("running");
188
+ });
189
+
190
+ it("conversation 缺省 → execute 入参 conversation===undefined(不误置 true)", async () => {
191
+ const svc = makeService();
192
+ await startHandler(svc, { task: "plain task", slug: "plain-pass" }, undefined);
193
+
194
+ expect(svc.execute).toHaveBeenCalledWith(
195
+ expect.objectContaining({ conversation: undefined, idleTimeoutMs: undefined }),
196
+ );
197
+ });
198
+ });
@@ -52,11 +52,15 @@ const { mockLoadAll, mockRegisterSubagentTool, mockScan } = vi.hoisted(() => ({
52
52
  mockRegisterSubagentTool: vi.fn(),
53
53
  }));
54
54
 
55
- // JsonlRunStore mock:loadAll 由各 test 配置。构造参数忽略(sessionDir/pi/ctx 都 mock 掉)
55
+ // JsonlRunStore mock:loadAll 由各 test 配置。构造参数忽略(sessionDir/pi/ctx 都 mock 掉)。
56
+ // dispose/flushPendingSaves:session_shutdown handler 会调 state.store.dispose()(W2C5),
57
+ // 本文件用例虽不触发 handler body,防御性补齐防 mock 缺方法 TypeError。
56
58
  vi.mock("../../orchestration/jsonl-run-store.ts", () => ({
57
59
  JsonlRunStore: class {
58
60
  loadAll = mockLoadAll;
59
61
  save = vi.fn(async () => {});
62
+ dispose = vi.fn(async () => {});
63
+ flushPendingSaves = vi.fn(async () => {});
60
64
  },
61
65
  }));
62
66
 
@@ -90,6 +94,8 @@ vi.mock("../subagent-service.ts", () => ({
90
94
  initSession = vi.fn();
91
95
  // W3: index.ts session_start 注入 UI handler 时调用
92
96
  setUiRequestHandler = vi.fn();
97
+ recoverManifestTmpFiles = vi.fn(async () => ({ deleted: 0, recovered: 0 }));
98
+ startGcTimer = vi.fn();
93
99
  },
94
100
  getSubagentService: () => null,
95
101
  setSubagentService: vi.fn(),
@@ -130,7 +136,7 @@ import { WorkflowRun } from "../../orchestration/models/workflow-run.ts";
130
136
  /** 构造一个可重水合的 WorkflowRun(用 WorkflowRun.reconstruct 跳过 I1 校验)。 */
131
137
  function makeRun(
132
138
  runId: string,
133
- status: "running" | "paused" | "done",
139
+ status: "running" | "done",
134
140
  reason?: "completed" | "failed" | "aborted" | "budget_limited" | "time_limited",
135
141
  ): WorkflowRunType {
136
142
  return WorkflowRun.reconstruct(