@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
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
);
|