@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.
- package/README.md +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +170 -14
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +25 -7
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +158 -52
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +598 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +44 -17
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +1 -1
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +29 -10
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/__tests__/spawn-args.test.ts
|
|
2
|
-
import {
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
|
|
7
|
-
import { afterEach, beforeEach, describe, expect, it
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
8
|
|
|
9
9
|
import { mirrorMainProcessFlags } from "../argv-mirror.ts";
|
|
10
10
|
import { MAX_FORK_DEPTH } from "../session-context-resolver.ts";
|
|
@@ -154,13 +154,14 @@ describe("buildSpawnArgs", () => {
|
|
|
154
154
|
// mirrorFlags 透传:子进程镜像主进程 extension/approve flag
|
|
155
155
|
// ============================================================
|
|
156
156
|
|
|
157
|
-
it("mirrorFlags 透传:noExtensions+approve+extensionPaths 全量 push(TC5)", () => {
|
|
157
|
+
it("mirrorFlags 透传:noExtensions+approve+noContextFiles+extensionPaths 全量 push(TC5)", () => {
|
|
158
158
|
const args = buildSpawnArgs({
|
|
159
159
|
...baseParams,
|
|
160
|
-
mirrorFlags: { noExtensions: true, approve: true, extensionPaths: ["/e1", "/e2"] },
|
|
160
|
+
mirrorFlags: { noExtensions: true, approve: true, noContextFiles: true, extensionPaths: ["/e1", "/e2"] },
|
|
161
161
|
});
|
|
162
162
|
expect(args).toContain("--no-extensions");
|
|
163
163
|
expect(args).toContain("--approve");
|
|
164
|
+
expect(args).toContain("--no-context-files");
|
|
164
165
|
// 每个 extension 独立 token,顺序保留
|
|
165
166
|
const extIdxs = args.map((a, i) => (a === "--extension" ? i : -1)).filter((i) => i >= 0);
|
|
166
167
|
expect(extIdxs).toHaveLength(2);
|
|
@@ -171,11 +172,12 @@ describe("buildSpawnArgs", () => {
|
|
|
171
172
|
it("mirrorFlags 全 false/空 → 不追加任何目标 flag(TC6)", () => {
|
|
172
173
|
const args = buildSpawnArgs({
|
|
173
174
|
...baseParams,
|
|
174
|
-
mirrorFlags: { noExtensions: false, approve: false, extensionPaths: [] },
|
|
175
|
+
mirrorFlags: { noExtensions: false, approve: false, noContextFiles: false, extensionPaths: [] },
|
|
175
176
|
});
|
|
176
177
|
expect(args).not.toContain("--no-extensions");
|
|
177
178
|
expect(args).not.toContain("--approve");
|
|
178
179
|
expect(args).not.toContain("--extension");
|
|
180
|
+
expect(args).not.toContain("--no-context-files");
|
|
179
181
|
// 仅基础参数
|
|
180
182
|
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
181
183
|
});
|
|
@@ -186,6 +188,70 @@ describe("buildSpawnArgs", () => {
|
|
|
186
188
|
expect(args).not.toContain("--extension");
|
|
187
189
|
expect(args).not.toContain("--no-extensions");
|
|
188
190
|
expect(args).not.toContain("--approve");
|
|
191
|
+
expect(args).not.toContain("--no-context-files");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("noContextFiles 镜像:主进程 argv 含 --no-context-files → 子进程 args 含该 flag", () => {
|
|
195
|
+
// 端到端链路断言:主 pi 进程以 --no-context-files 启动(用户 opt-out context files)
|
|
196
|
+
// → mirrorMainProcessFlags 解析 → buildSpawnArgs 产出同 flag 给每个 subagent。
|
|
197
|
+
// 缺此镜像时,--extension 镜像带入的 xyz-system-prompt-extension.js 检查子进程
|
|
198
|
+
// 自己的 argv(无 flag)→ 全局 AGENTS.md 注入在 subagent system prompt 照常生效,
|
|
199
|
+
// 用户 opt-out 被绕过。
|
|
200
|
+
const mainArgv = [
|
|
201
|
+
"bun", "/pi", "--mode", "rpc", "--no-context-files",
|
|
202
|
+
"--extension", "/path/xyz-system-prompt-extension.js",
|
|
203
|
+
];
|
|
204
|
+
const args = buildSpawnArgs({ ...baseParams, mirrorFlags: mirrorMainProcessFlags(mainArgv) });
|
|
205
|
+
expect(args).toContain("--no-context-files");
|
|
206
|
+
// extension 镜像照常(两者共存才构成完整契约)
|
|
207
|
+
const extIdx = args.indexOf("--extension");
|
|
208
|
+
expect(args[extIdx + 1]).toBe("/path/xyz-system-prompt-extension.js");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("noContextFiles 不镜像:主进程 argv 不含 flag → 子进程 args 不含(默认行为不变)", () => {
|
|
212
|
+
const mainArgv = ["bun", "/pi", "--mode", "rpc"];
|
|
213
|
+
const args = buildSpawnArgs({ ...baseParams, mirrorFlags: mirrorMainProcessFlags(mainArgv) });
|
|
214
|
+
expect(args).not.toContain("--no-context-files");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ============================================================
|
|
218
|
+
// sessionFile(M1 resume 基建):--session <file> 紧跟 --session-dir
|
|
219
|
+
// ============================================================
|
|
220
|
+
|
|
221
|
+
it("sessionFile 存在 → 紧跟 --session-dir 追加 --session <file>", () => {
|
|
222
|
+
const args = buildSpawnArgs({
|
|
223
|
+
...baseParams,
|
|
224
|
+
sessionFile: "/sessions/sub/abc.jsonl",
|
|
225
|
+
});
|
|
226
|
+
const idx = args.indexOf("--session");
|
|
227
|
+
expect(idx).toBeGreaterThan(-1);
|
|
228
|
+
expect(args[idx + 1]).toBe("/sessions/sub/abc.jsonl");
|
|
229
|
+
// 位置紧跟 --session-dir <dir> 之后(--model 之前)
|
|
230
|
+
const sessionDirIdx = args.indexOf("--session-dir");
|
|
231
|
+
expect(idx).toBe(sessionDirIdx + 2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("sessionFile undefined → 不含 --session(向后兼容)", () => {
|
|
235
|
+
const args = buildSpawnArgs(baseParams);
|
|
236
|
+
expect(args).not.toContain("--session");
|
|
237
|
+
expect(args).toEqual(["--mode", "rpc", "--session-dir", "/sessions/dir"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("sessionFile + model + thinkingLevel → 三者都进 args(resume 全参数)", () => {
|
|
241
|
+
const args = buildSpawnArgs({
|
|
242
|
+
...baseParams,
|
|
243
|
+
sessionFile: "/sessions/sub/resume.jsonl",
|
|
244
|
+
model: "anthropic/claude",
|
|
245
|
+
thinkingLevel: "high",
|
|
246
|
+
});
|
|
247
|
+
// --session <file>
|
|
248
|
+
const sessionIdx = args.indexOf("--session");
|
|
249
|
+
expect(sessionIdx).toBeGreaterThan(-1);
|
|
250
|
+
expect(args[sessionIdx + 1]).toBe("/sessions/sub/resume.jsonl");
|
|
251
|
+
// --model provider/id:level(thinkingLevel 作 model 后缀)
|
|
252
|
+
const modelIdx = args.indexOf("--model");
|
|
253
|
+
expect(modelIdx).toBeGreaterThan(-1);
|
|
254
|
+
expect(args[modelIdx + 1]).toBe("anthropic/claude:high");
|
|
189
255
|
});
|
|
190
256
|
});
|
|
191
257
|
|
|
@@ -199,14 +265,14 @@ describe("mirrorMainProcessFlags", () => {
|
|
|
199
265
|
"bun", "/pi", "--mode", "rpc", "--no-extensions", "--approve",
|
|
200
266
|
"--extension", "/a", "--extension", "/b",
|
|
201
267
|
]);
|
|
202
|
-
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/a", "/b"] });
|
|
268
|
+
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/a", "/b"], noContextFiles: false });
|
|
203
269
|
});
|
|
204
270
|
|
|
205
271
|
it("--extension=path 等号形式 + 短形式 -e/-ne/-a(TC2)", () => {
|
|
206
272
|
const r = mirrorMainProcessFlags([
|
|
207
273
|
"bun", "/pi", "--extension=/x", "-ne", "-a",
|
|
208
274
|
]);
|
|
209
|
-
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/x"] });
|
|
275
|
+
expect(r).toEqual({ noExtensions: true, approve: true, extensionPaths: ["/x"], noContextFiles: false });
|
|
210
276
|
});
|
|
211
277
|
|
|
212
278
|
it("混合形式(空格 + 等号),顺序保留(TC3)", () => {
|
|
@@ -216,11 +282,29 @@ describe("mirrorMainProcessFlags", () => {
|
|
|
216
282
|
expect(r.extensionPaths).toEqual(["/a", "/b", "/c"]);
|
|
217
283
|
expect(r.noExtensions).toBe(false);
|
|
218
284
|
expect(r.approve).toBe(false);
|
|
285
|
+
expect(r.noContextFiles).toBe(false);
|
|
219
286
|
});
|
|
220
287
|
|
|
221
288
|
it("无目标 flag → 全空/全 false(向后兼容,TC4)", () => {
|
|
222
289
|
const r = mirrorMainProcessFlags(["bun", "/pi", "--mode", "rpc"]);
|
|
223
|
-
expect(r).toEqual({ noExtensions: false, approve: false, extensionPaths: [] });
|
|
290
|
+
expect(r).toEqual({ noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("--no-context-files 长形式解析 → noContextFiles: true", () => {
|
|
294
|
+
const r = mirrorMainProcessFlags([
|
|
295
|
+
"bun", "/pi", "--mode", "rpc", "--no-context-files",
|
|
296
|
+
"--extension", "/path/xyz-system-prompt-extension.js",
|
|
297
|
+
]);
|
|
298
|
+
expect(r.noContextFiles).toBe(true);
|
|
299
|
+
// 与 extension 镜像共存(实际 xyz-agent 启动形态:两 flag 同时出现)
|
|
300
|
+
expect(r.extensionPaths).toEqual(["/path/xyz-system-prompt-extension.js"]);
|
|
301
|
+
expect(r.noExtensions).toBe(false);
|
|
302
|
+
expect(r.approve).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("-nc 短形式解析 → noContextFiles: true(pi CLI 等价短形式)", () => {
|
|
306
|
+
const r = mirrorMainProcessFlags(["bun", "/pi", "--mode", "rpc", "-nc"]);
|
|
307
|
+
expect(r.noContextFiles).toBe(true);
|
|
224
308
|
});
|
|
225
309
|
|
|
226
310
|
it("不误吃其他 flag 值与 positional 参数(TC8)", () => {
|
|
@@ -233,9 +317,9 @@ describe("mirrorMainProcessFlags", () => {
|
|
|
233
317
|
});
|
|
234
318
|
|
|
235
319
|
it("空 argv / 仅前导两项 → 全空", () => {
|
|
236
|
-
expect(mirrorMainProcessFlags([])).toEqual({ noExtensions: false, approve: false, extensionPaths: [] });
|
|
320
|
+
expect(mirrorMainProcessFlags([])).toEqual({ noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false });
|
|
237
321
|
expect(mirrorMainProcessFlags(["bun", "/pi"])).toEqual({
|
|
238
|
-
noExtensions: false, approve: false, extensionPaths: [],
|
|
322
|
+
noExtensions: false, approve: false, extensionPaths: [], noContextFiles: false,
|
|
239
323
|
});
|
|
240
324
|
});
|
|
241
325
|
|
|
@@ -257,6 +341,7 @@ describe("mirrorMainProcessFlags", () => {
|
|
|
257
341
|
describe("buildEnvBlock", () => {
|
|
258
342
|
// buildEnvBlock 内部按 cwd 缓存 git branch(模块级 Map),用真实 git 仓库测最稳。
|
|
259
343
|
// 用临时 git 仓库隔离,避免污染主仓库 branch 缓存。
|
|
344
|
+
// buildEnvBlock 已异步化(execFile + Promise 包装),所有直接调用需 await。
|
|
260
345
|
let tmpGitRepo: string;
|
|
261
346
|
const testBranch = "test-env-branch";
|
|
262
347
|
|
|
@@ -265,70 +350,70 @@ describe("buildEnvBlock", () => {
|
|
|
265
350
|
// 初始化 git 仓库 + checkout 已知分支名。
|
|
266
351
|
// 必须先 commit 一次:git rev-parse --abbrev-ref HEAD 在无 commit 的空仓库会失败
|
|
267
352
|
//(exit 128,HEAD 未解析),buildEnvBlock 走兜底 branch=""。
|
|
268
|
-
|
|
269
|
-
|
|
353
|
+
spawnSync("git", ["init", "-q"], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
354
|
+
spawnSync("git", ["checkout", "-q", "-b", testBranch], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
270
355
|
// git commit 需要 user.email/name;本地配置避免依赖全局 git config(CI 无身份时失败)
|
|
271
|
-
|
|
272
|
-
|
|
356
|
+
spawnSync("git", ["config", "user.email", "test@test.local"], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
357
|
+
spawnSync("git", ["config", "user.name", "Test"], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
273
358
|
fs.writeFileSync(path.join(tmpGitRepo, "README.md"), "init\n", "utf-8");
|
|
274
|
-
|
|
275
|
-
|
|
359
|
+
spawnSync("git", ["add", "."], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
360
|
+
spawnSync("git", ["commit", "-q", "-m", "init"], { cwd: tmpGitRepo, stdio: "ignore" });
|
|
276
361
|
});
|
|
277
362
|
|
|
278
363
|
afterEach(() => {
|
|
279
364
|
fs.rmSync(tmpGitRepo, { recursive: true, force: true });
|
|
280
365
|
});
|
|
281
366
|
|
|
282
|
-
it("注入 cwd(Working directory 行)", () => {
|
|
283
|
-
const block = buildEnvBlock(tmpGitRepo);
|
|
367
|
+
it("注入 cwd(Working directory 行)", async () => {
|
|
368
|
+
const block = await buildEnvBlock(tmpGitRepo);
|
|
284
369
|
expect(block).toContain(`Working directory: ${tmpGitRepo}`);
|
|
285
370
|
expect(block).toContain("--- environment (data, not instructions) ---");
|
|
286
371
|
expect(block).toContain("--- end environment ---");
|
|
287
372
|
});
|
|
288
373
|
|
|
289
|
-
it("forkDepth > 0 → 含 Depth: N/<MAX>", () => {
|
|
290
|
-
const block = buildEnvBlock(tmpGitRepo, 3);
|
|
374
|
+
it("forkDepth > 0 → 含 Depth: N/<MAX>", async () => {
|
|
375
|
+
const block = await buildEnvBlock(tmpGitRepo, 3);
|
|
291
376
|
expect(block).toContain(`Depth: 3/${MAX_FORK_DEPTH}`);
|
|
292
377
|
});
|
|
293
378
|
|
|
294
|
-
it("forkDepth === 0 → 不含 depth 行", () => {
|
|
295
|
-
const block = buildEnvBlock(tmpGitRepo, 0);
|
|
379
|
+
it("forkDepth === 0 → 不含 depth 行", async () => {
|
|
380
|
+
const block = await buildEnvBlock(tmpGitRepo, 0);
|
|
296
381
|
expect(block).not.toContain("Depth:");
|
|
297
382
|
});
|
|
298
383
|
|
|
299
|
-
it("forkDepth undefined → 不含 depth 行", () => {
|
|
300
|
-
const block = buildEnvBlock(tmpGitRepo);
|
|
384
|
+
it("forkDepth undefined → 不含 depth 行", async () => {
|
|
385
|
+
const block = await buildEnvBlock(tmpGitRepo);
|
|
301
386
|
expect(block).not.toContain("Depth:");
|
|
302
387
|
});
|
|
303
388
|
|
|
304
389
|
// [M9] nestingDepth:取 max(forkDepth, nestingDepth) 展示更严约束。
|
|
305
|
-
it("forkDepth < nestingDepth → 展示 max(nestingDepth 更严)", () => {
|
|
390
|
+
it("forkDepth < nestingDepth → 展示 max(nestingDepth 更严)", async () => {
|
|
306
391
|
// forkDepth=1(最内 fork),nestingDepth=5(通用嵌套已深)→ 展示 5
|
|
307
|
-
const block = buildEnvBlock(tmpGitRepo, 1, 5);
|
|
392
|
+
const block = await buildEnvBlock(tmpGitRepo, 1, 5);
|
|
308
393
|
expect(block).toContain(`Depth: 5/${MAX_FORK_DEPTH}`);
|
|
309
394
|
expect(block).not.toContain(`Depth: 1/${MAX_FORK_DEPTH}`);
|
|
310
395
|
});
|
|
311
396
|
|
|
312
|
-
it("forkDepth > nestingDepth → 展示 max(forkDepth 更严)", () => {
|
|
313
|
-
const block = buildEnvBlock(tmpGitRepo, 7, 2);
|
|
397
|
+
it("forkDepth > nestingDepth → 展示 max(forkDepth 更严)", async () => {
|
|
398
|
+
const block = await buildEnvBlock(tmpGitRepo, 7, 2);
|
|
314
399
|
expect(block).toContain(`Depth: 7/${MAX_FORK_DEPTH}`);
|
|
315
400
|
});
|
|
316
401
|
|
|
317
|
-
it("forkDepth=0 + nestingDepth>0 → 展示 nestingDepth(非 fork 嵌套也计入)", () => {
|
|
402
|
+
it("forkDepth=0 + nestingDepth>0 → 展示 nestingDepth(非 fork 嵌套也计入)", async () => {
|
|
318
403
|
// 非 fork 但有嵌套(如顶层 → 子 → 孙),nestingDepth=2 应展示
|
|
319
|
-
const block = buildEnvBlock(tmpGitRepo, undefined, 2);
|
|
404
|
+
const block = await buildEnvBlock(tmpGitRepo, undefined, 2);
|
|
320
405
|
expect(block).toContain(`Depth: 2/${MAX_FORK_DEPTH}`);
|
|
321
406
|
});
|
|
322
407
|
|
|
323
|
-
it("git branch 存在 → 含 Git branch 行", () => {
|
|
324
|
-
const block = buildEnvBlock(tmpGitRepo);
|
|
408
|
+
it("git branch 存在 → 含 Git branch 行", async () => {
|
|
409
|
+
const block = await buildEnvBlock(tmpGitRepo);
|
|
325
410
|
expect(block).toContain(`Git branch: ${testBranch}`);
|
|
326
411
|
});
|
|
327
412
|
|
|
328
|
-
it("非 git 目录 → 不含 Git branch 行(git 失败兜底空串)", () => {
|
|
413
|
+
it("非 git 目录 → 不含 Git branch 行(git 失败兜底空串)", async () => {
|
|
329
414
|
const nonGitDir = fs.mkdtempSync(path.join(os.tmpdir(), "envblock-nogit-"));
|
|
330
415
|
try {
|
|
331
|
-
const block = buildEnvBlock(nonGitDir);
|
|
416
|
+
const block = await buildEnvBlock(nonGitDir);
|
|
332
417
|
expect(block).not.toContain("Git branch:");
|
|
333
418
|
// 但仍含 working directory(环境块始终输出)
|
|
334
419
|
expect(block).toContain(`Working directory: ${nonGitDir}`);
|
|
@@ -337,20 +422,13 @@ describe("buildEnvBlock", () => {
|
|
|
337
422
|
}
|
|
338
423
|
});
|
|
339
424
|
|
|
340
|
-
it("git 失败(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const block = buildEnvBlock("/some/cwd");
|
|
350
|
-
expect(block).not.toContain("Git branch:");
|
|
351
|
-
expect(block).toContain("Working directory: /some/cwd");
|
|
352
|
-
} finally {
|
|
353
|
-
mockExec.mockRestore();
|
|
354
|
-
}
|
|
425
|
+
it("git 失败(execFile err 回调)→ 不崩,静默省略 branch", async () => {
|
|
426
|
+
// 旧版 spy 挂在测试本地对象字面量上(同名同步 git API 的对象包装),从未真正拦截
|
|
427
|
+
// session-runner 的模块绑定——此前通过是靠 /some/cwd 不存在使真实 git 失败的副作用。
|
|
428
|
+
// 异步化后改为显式用不存在的 cwd 触发 execFile err 回调(err → reject → catch →
|
|
429
|
+
// branch=""),覆盖同一条失败路径,断言不变。
|
|
430
|
+
const block = await buildEnvBlock("/some/cwd");
|
|
431
|
+
expect(block).not.toContain("Git branch:");
|
|
432
|
+
expect(block).toContain("Working directory: /some/cwd");
|
|
355
433
|
});
|
|
356
434
|
});
|
|
@@ -30,6 +30,7 @@ vi.mock("node:child_process", async () => {
|
|
|
30
30
|
class FakeChild extends EventEmitter {
|
|
31
31
|
pid = 12345;
|
|
32
32
|
stdout = new PassThrough();
|
|
33
|
+
stdin = new PassThrough(); // runSpawn 注册 stdin EPIPE handler(session-runner),FakeChild 必须提供
|
|
33
34
|
stderr = new PassThrough();
|
|
34
35
|
killed = false;
|
|
35
36
|
killSignal: string | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/execution/__tests__/spawned-children.test.ts
|
|
2
|
+
//
|
|
3
|
+
// spawnedChildren Map + getChildByRecord + killAllSpawnedChildren 单元测试(M2-B1)。
|
|
4
|
+
//
|
|
5
|
+
// M2-B1 把 spawnedChildren 从 Set<ChildProcess> 改为 Map<recordId, ChildProcess>,
|
|
6
|
+
// 建立 record→child 映射(busy 投递定位活进程用,设计决策 6)。本文件测 Map API 核心行为:
|
|
7
|
+
// - getChildByRecord:set 后能查到;delete 后查不到;未注册返回 undefined
|
|
8
|
+
// - killAllSpawnedChildren:遍历 .values() kill + clear;跳过 child.killed=true;单 child kill 抛错不阻断
|
|
9
|
+
//
|
|
10
|
+
// 直接操作模块级 spawnedChildren(afterEach clear 防泄漏),不依赖 runSpawn/spawn/fs。
|
|
11
|
+
|
|
12
|
+
import type { ChildProcess } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
|
|
16
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
17
|
+
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() },
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({ getLogger: () => loggerMock }));
|
|
20
|
+
|
|
21
|
+
import { getChildByRecord, killAllSpawnedChildren, spawnedChildren } from "../session-runner.ts";
|
|
22
|
+
|
|
23
|
+
/** 假 child:只关心 killed 标记 + kill 调用(killAllSpawnedChildren 遍历用)。 */
|
|
24
|
+
function makeFakeChild(killed = false): ChildProcess & { kill: ReturnType<typeof vi.fn> } {
|
|
25
|
+
return { killed, kill: vi.fn() } as unknown as ChildProcess & { kill: ReturnType<typeof vi.fn> };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("spawnedChildren Map + getChildByRecord (M2-B1)", () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
spawnedChildren.clear();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("set(recordId, child) 后 getChildByRecord 能查到;未注册返回 undefined", () => {
|
|
34
|
+
const child = makeFakeChild();
|
|
35
|
+
spawnedChildren.set("rec-1", child);
|
|
36
|
+
expect(getChildByRecord("rec-1")).toBe(child);
|
|
37
|
+
expect(getChildByRecord("rec-2")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("delete(recordId) 后 getChildByRecord 返回 undefined(close/error 回调语义)", () => {
|
|
41
|
+
const child = makeFakeChild();
|
|
42
|
+
spawnedChildren.set("rec-1", child);
|
|
43
|
+
spawnedChildren.delete("rec-1");
|
|
44
|
+
expect(getChildByRecord("rec-1")).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("killAllSpawnedChildren 遍历 .values() 对每个未 killed child kill + clear", () => {
|
|
48
|
+
const c1 = makeFakeChild();
|
|
49
|
+
const c2 = makeFakeChild();
|
|
50
|
+
spawnedChildren.set("rec-a", c1);
|
|
51
|
+
spawnedChildren.set("rec-b", c2);
|
|
52
|
+
|
|
53
|
+
const n = killAllSpawnedChildren();
|
|
54
|
+
|
|
55
|
+
expect(n).toBe(2);
|
|
56
|
+
expect(c1.kill).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(c2.kill).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(spawnedChildren.size).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("killAllSpawnedChildren 跳过 child.killed=true(不重复 kill 已 kill 的)", () => {
|
|
62
|
+
const alive = makeFakeChild(false);
|
|
63
|
+
const alreadyKilled = makeFakeChild(true);
|
|
64
|
+
spawnedChildren.set("rec-1", alive);
|
|
65
|
+
spawnedChildren.set("rec-2", alreadyKilled);
|
|
66
|
+
|
|
67
|
+
const n = killAllSpawnedChildren();
|
|
68
|
+
|
|
69
|
+
expect(n).toBe(1);
|
|
70
|
+
expect(alive.kill).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(alreadyKilled.kill).not.toHaveBeenCalled();
|
|
72
|
+
expect(spawnedChildren.size).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("killAllSpawnedChildren 对单个 child kill 抛错不阻断其他(best-effort catch)", () => {
|
|
76
|
+
const ok = makeFakeChild();
|
|
77
|
+
const throwy = makeFakeChild();
|
|
78
|
+
throwy.kill.mockImplementation(() => {
|
|
79
|
+
throw new Error("ESRCH");
|
|
80
|
+
});
|
|
81
|
+
spawnedChildren.set("rec-1", ok);
|
|
82
|
+
spawnedChildren.set("rec-2", throwy);
|
|
83
|
+
|
|
84
|
+
const n = killAllSpawnedChildren();
|
|
85
|
+
|
|
86
|
+
// throwy 抛错被 catch,不计入 n;ok 正常 kill。两者都调过 kill,Map 已 clear。
|
|
87
|
+
expect(n).toBe(1);
|
|
88
|
+
expect(ok.kill).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(throwy.kill).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(spawnedChildren.size).toBe(0);
|
|
91
|
+
});
|
|
92
|
+
});
|