@zhushanwen/pi-subagent-workflow 0.1.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/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// src/execution/__tests__/subprocess-agent-runner.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Wave 4: SubprocessAgentRunner 委托重写测试
|
|
4
|
+
//
|
|
5
|
+
// 覆盖 test-matrix 用例:
|
|
6
|
+
// T3.1 (正常): SAR 委托 executeAndAwait 返回 content
|
|
7
|
+
// T3.2 (正常): parsedOutput 透传
|
|
8
|
+
// T3.4 (边界): cwd 透传(非 git worktree)
|
|
9
|
+
// T3.5 (边界): model 填底(opts.model 空 → ctxModel)
|
|
10
|
+
// T3.6 (异常): timeoutMs 超时 → signal abort → error
|
|
11
|
+
// T3.7 (正常): onEvent 桥接 AgentEvent 透传
|
|
12
|
+
// T3.17 (NFR): mergeTimeoutSignal listener 清理
|
|
13
|
+
// T3.18 (NFR): dispose 兜底覆盖(delegate 后子进程进 spawnedChildren)
|
|
14
|
+
// T3.19 (NFR): AgentCallOpts→ExecuteOptions 映射保真
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it, vi } from "vitest";
|
|
17
|
+
|
|
18
|
+
import { SubprocessAgentRunner } from "../subprocess-agent-runner.ts";
|
|
19
|
+
import type { SubprocessAgentRunnerDeps } from "../subprocess-agent-runner.ts";
|
|
20
|
+
import type { AgentCallOpts, AgentResult } from "../../orchestration/models/types.ts";
|
|
21
|
+
|
|
22
|
+
// ── 测试辅助 ──
|
|
23
|
+
|
|
24
|
+
/** 构造 mock SubagentService.executeAndAwait 返回值 */
|
|
25
|
+
function makeMockResult(overrides: Partial<AgentResult> = {}): AgentResult {
|
|
26
|
+
return {
|
|
27
|
+
content: "OK",
|
|
28
|
+
parsedOutput: undefined,
|
|
29
|
+
durationMs: 100,
|
|
30
|
+
error: undefined,
|
|
31
|
+
sessionId: undefined,
|
|
32
|
+
toolCalls: [],
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 创建 mock SubagentService(只实现 executeAndAwait) */
|
|
38
|
+
function createMockService(impl?: typeof vi.fn) {
|
|
39
|
+
const executeAndAwait = impl ?? vi.fn().mockResolvedValue(makeMockResult());
|
|
40
|
+
return { executeAndAwait } as unknown as {
|
|
41
|
+
executeAndAwait: (
|
|
42
|
+
opts: Record<string, unknown>,
|
|
43
|
+
signal?: AbortSignal,
|
|
44
|
+
onEvent?: (e: Record<string, unknown>) => void,
|
|
45
|
+
) => Promise<AgentResult>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeBaseOpts(): AgentCallOpts {
|
|
50
|
+
return {
|
|
51
|
+
prompt: "test task",
|
|
52
|
+
agent: "worker",
|
|
53
|
+
cwd: "/some/path",
|
|
54
|
+
schema: undefined,
|
|
55
|
+
model: undefined,
|
|
56
|
+
scene: undefined,
|
|
57
|
+
description: undefined,
|
|
58
|
+
timeoutMs: undefined,
|
|
59
|
+
skill: undefined,
|
|
60
|
+
skillPath: undefined,
|
|
61
|
+
systemPromptFiles: undefined,
|
|
62
|
+
schemaEnv: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── T3.1: 正常路径 ──
|
|
67
|
+
|
|
68
|
+
describe("SubprocessAgentRunner (wave-4 delegate)", () => {
|
|
69
|
+
// ────────────────────────────────────────────────
|
|
70
|
+
// T3.1: 正常路径 — SAR 委托 executeAndAwait 返回 content
|
|
71
|
+
// ────────────────────────────────────────────────
|
|
72
|
+
describe("T3.1 主流程", () => {
|
|
73
|
+
it("委托 executeAndAwait 并返回 content", async () => {
|
|
74
|
+
const mockService = createMockService(
|
|
75
|
+
vi.fn().mockResolvedValue(makeMockResult({ content: "hello world" })),
|
|
76
|
+
);
|
|
77
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
78
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
79
|
+
|
|
80
|
+
const opts = makeBaseOpts();
|
|
81
|
+
const signal = new AbortController().signal;
|
|
82
|
+
|
|
83
|
+
const result = await sar.run(opts, signal);
|
|
84
|
+
|
|
85
|
+
expect(result.content).toBe("hello world");
|
|
86
|
+
expect(mockService.executeAndAwait).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("不 reject — 失败信息入 result.error", async () => {
|
|
90
|
+
const mockService = createMockService(
|
|
91
|
+
vi.fn().mockResolvedValue(makeMockResult({ error: "some error" })),
|
|
92
|
+
);
|
|
93
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
94
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
95
|
+
|
|
96
|
+
const result = await sar.run(makeBaseOpts(), new AbortController().signal);
|
|
97
|
+
expect(result.error).toBe("some error");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ────────────────────────────────────────────────
|
|
102
|
+
// T3.2: parsedOutput 透传
|
|
103
|
+
// ────────────────────────────────────────────────
|
|
104
|
+
describe("T3.2 parsedOutput 透传", () => {
|
|
105
|
+
it("parsedOutput 直通", async () => {
|
|
106
|
+
const parsedData = { x: 1, y: 2 };
|
|
107
|
+
const mockService = createMockService(
|
|
108
|
+
vi.fn().mockResolvedValue(makeMockResult({ content: "ok", parsedOutput: parsedData })),
|
|
109
|
+
);
|
|
110
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
111
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
112
|
+
|
|
113
|
+
const result = await sar.run(makeBaseOpts(), new AbortController().signal);
|
|
114
|
+
expect(result.parsedOutput).toEqual(parsedData);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ────────────────────────────────────────────────
|
|
119
|
+
// T3.4: cwd 透传
|
|
120
|
+
// ────────────────────────────────────────────────
|
|
121
|
+
describe("T3.4 cwd 透传", () => {
|
|
122
|
+
it("cwd 传入 executeAndAwait 的 ExecuteOptions", async () => {
|
|
123
|
+
let capturedOpts: Record<string, unknown> | undefined;
|
|
124
|
+
const mockService = createMockService(
|
|
125
|
+
vi.fn().mockImplementation((opts: Record<string, unknown>) => {
|
|
126
|
+
capturedOpts = opts;
|
|
127
|
+
return Promise.resolve(makeMockResult());
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
131
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
132
|
+
|
|
133
|
+
const opts = { ...makeBaseOpts(), cwd: "/custom/cwd" };
|
|
134
|
+
await sar.run(opts, new AbortController().signal);
|
|
135
|
+
|
|
136
|
+
expect(capturedOpts).toBeDefined();
|
|
137
|
+
expect(capturedOpts!.cwd).toBe("/custom/cwd");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ────────────────────────────────────────────────
|
|
142
|
+
// T3.5: model 填底
|
|
143
|
+
// ────────────────────────────────────────────────
|
|
144
|
+
describe("T3.5 model 填底 (D-008)", () => {
|
|
145
|
+
it("opts.model 优先", async () => {
|
|
146
|
+
let capturedOpts: Record<string, unknown> | undefined;
|
|
147
|
+
const mockService = createMockService(
|
|
148
|
+
vi.fn().mockImplementation((opts: Record<string, unknown>) => {
|
|
149
|
+
capturedOpts = opts;
|
|
150
|
+
return Promise.resolve(makeMockResult());
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
const ctxModel = { id: "ctx-model", provider: "test", input: [] };
|
|
154
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel };
|
|
155
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
156
|
+
|
|
157
|
+
const opts = { ...makeBaseOpts(), model: "explicit-model" };
|
|
158
|
+
await sar.run(opts, new AbortController().signal);
|
|
159
|
+
|
|
160
|
+
expect(capturedOpts!.model).toBe("explicit-model");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("opts.model 空 → ctxModel", async () => {
|
|
164
|
+
let capturedOpts: Record<string, unknown> | undefined;
|
|
165
|
+
const mockService = createMockService(
|
|
166
|
+
vi.fn().mockImplementation((opts: Record<string, unknown>) => {
|
|
167
|
+
capturedOpts = opts;
|
|
168
|
+
return Promise.resolve(makeMockResult());
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
const ctxModel = { id: "ctx-model", provider: "test", input: [] };
|
|
172
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService, ctxModel };
|
|
173
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
174
|
+
|
|
175
|
+
const opts = { ...makeBaseOpts(), model: undefined };
|
|
176
|
+
await sar.run(opts, new AbortController().signal);
|
|
177
|
+
|
|
178
|
+
expect(capturedOpts!.model).toBe("ctx-model");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ────────────────────────────────────────────────
|
|
183
|
+
// T3.6: timeoutMs 超时 → signal abort → error
|
|
184
|
+
// ────────────────────────────────────────────────
|
|
185
|
+
describe("T3.6 timeoutMs 超时", () => {
|
|
186
|
+
it("timeoutMs > 0 → merged signal 传给 executeAndAwait", async () => {
|
|
187
|
+
let capturedSignal: AbortSignal | undefined;
|
|
188
|
+
const mockService = createMockService(
|
|
189
|
+
vi.fn().mockImplementation((_opts: Record<string, unknown>, signal?: AbortSignal) => {
|
|
190
|
+
capturedSignal = signal;
|
|
191
|
+
return Promise.resolve(makeMockResult());
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
195
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
196
|
+
|
|
197
|
+
const opts = { ...makeBaseOpts(), timeoutMs: 5000 };
|
|
198
|
+
await sar.run(opts, new AbortController().signal);
|
|
199
|
+
|
|
200
|
+
// 有 timeoutMs 时 merged signal 不等于原始 signal
|
|
201
|
+
expect(capturedSignal).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("timeoutMs 到期 → merged signal.aborted=true", async () => {
|
|
205
|
+
vi.useFakeTimers();
|
|
206
|
+
let capturedSignal: AbortSignal | undefined;
|
|
207
|
+
const mockService = createMockService(
|
|
208
|
+
vi.fn().mockImplementation((_opts: Record<string, unknown>, signal?: AbortSignal) => {
|
|
209
|
+
capturedSignal = signal;
|
|
210
|
+
return Promise.resolve(makeMockResult());
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
214
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
215
|
+
|
|
216
|
+
const opts = { ...makeBaseOpts(), timeoutMs: 50 };
|
|
217
|
+
sar.run(opts, new AbortController().signal);
|
|
218
|
+
|
|
219
|
+
// 推进 51ms 后 merged signal 应 abort
|
|
220
|
+
vi.advanceTimersByTime(51);
|
|
221
|
+
|
|
222
|
+
expect(capturedSignal?.aborted).toBe(true);
|
|
223
|
+
vi.useRealTimers();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ────────────────────────────────────────────────
|
|
228
|
+
// T3.7: onEvent 桥接
|
|
229
|
+
// ────────────────────────────────────────────────
|
|
230
|
+
describe("T3.7 onEvent 桥接", () => {
|
|
231
|
+
it("executeAndAwait 的 onEvent 透传到 workflow onEvent", async () => {
|
|
232
|
+
const mockService = createMockService(
|
|
233
|
+
vi.fn().mockImplementation(
|
|
234
|
+
(_opts: Record<string, unknown>, _signal?: AbortSignal, onEvent?: (e: Record<string, unknown>) => void) => {
|
|
235
|
+
// 模拟 executeAndAwait 触发 onEvent
|
|
236
|
+
onEvent?.({ type: "tool_start", toolName: "read", args: { path: "/a.txt" } });
|
|
237
|
+
return Promise.resolve(makeMockResult());
|
|
238
|
+
},
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
242
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
243
|
+
|
|
244
|
+
const workflowOnEvent = vi.fn();
|
|
245
|
+
await sar.run(makeBaseOpts(), new AbortController().signal, workflowOnEvent);
|
|
246
|
+
|
|
247
|
+
// workflow 的 onEvent 应被调用
|
|
248
|
+
expect(workflowOnEvent).toHaveBeenCalledTimes(1);
|
|
249
|
+
expect(workflowOnEvent).toHaveBeenCalledWith({
|
|
250
|
+
type: "tool_start",
|
|
251
|
+
toolName: "read",
|
|
252
|
+
args: { path: "/a.txt" },
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("无 workflow onEvent → 不传 bridgedOnEvent", async () => {
|
|
257
|
+
let bridgedOnEvent: ((e: Record<string, unknown>) => void) | undefined;
|
|
258
|
+
const mockService = createMockService(
|
|
259
|
+
vi.fn().mockImplementation(
|
|
260
|
+
(_opts: Record<string, unknown>, _signal?: AbortSignal, onEvent?: (e: Record<string, unknown>) => void) => {
|
|
261
|
+
bridgedOnEvent = onEvent;
|
|
262
|
+
return Promise.resolve(makeMockResult());
|
|
263
|
+
},
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
267
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
268
|
+
|
|
269
|
+
await sar.run(makeBaseOpts(), new AbortController().signal);
|
|
270
|
+
|
|
271
|
+
// 不传 workflow onEvent → bridgedOnEvent 应为 undefined
|
|
272
|
+
expect(bridgedOnEvent).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ────────────────────────────────────────────────
|
|
277
|
+
// T3.17: mergeTimeoutSignal listener 清理
|
|
278
|
+
// ────────────────────────────────────────────────
|
|
279
|
+
describe("T3.17 mergeTimeoutSignal listener 清理", () => {
|
|
280
|
+
it("外部 signal abort → timer 清理", async () => {
|
|
281
|
+
vi.useFakeTimers();
|
|
282
|
+
const mockService = createMockService(
|
|
283
|
+
vi.fn().mockResolvedValue(makeMockResult()),
|
|
284
|
+
);
|
|
285
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
286
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
287
|
+
|
|
288
|
+
const ctrl = new AbortController();
|
|
289
|
+
const opts = { ...makeBaseOpts(), timeoutMs: 5000 };
|
|
290
|
+
|
|
291
|
+
// 启动 run(不 await,让它在后台)
|
|
292
|
+
const runPromise = sar.run(opts, ctrl.signal);
|
|
293
|
+
|
|
294
|
+
// 外部 abort
|
|
295
|
+
ctrl.abort();
|
|
296
|
+
|
|
297
|
+
await runPromise;
|
|
298
|
+
|
|
299
|
+
// 推进时间——timer 应已清理,不会造成副作用
|
|
300
|
+
vi.advanceTimersByTime(6000);
|
|
301
|
+
// 无异常 = listener 已正确清理
|
|
302
|
+
vi.useRealTimers();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ────────────────────────────────────────────────
|
|
307
|
+
// T3.19: AgentCallOpts → ExecuteOptions 映射保真
|
|
308
|
+
// ────────────────────────────────────────────────
|
|
309
|
+
describe("T3.19 映射保真", () => {
|
|
310
|
+
it("prompt → task, agent → agent, schemaEnv 透传", async () => {
|
|
311
|
+
let capturedOpts: Record<string, unknown> | undefined;
|
|
312
|
+
const mockService = createMockService(
|
|
313
|
+
vi.fn().mockImplementation((opts: Record<string, unknown>) => {
|
|
314
|
+
capturedOpts = opts;
|
|
315
|
+
return Promise.resolve(makeMockResult());
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
319
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
320
|
+
|
|
321
|
+
const opts: AgentCallOpts = {
|
|
322
|
+
...makeBaseOpts(),
|
|
323
|
+
prompt: "do the thing",
|
|
324
|
+
agent: "code-reviewer",
|
|
325
|
+
schemaEnv: '{"type":"object"}',
|
|
326
|
+
skillPath: "/skills/code-review.md",
|
|
327
|
+
};
|
|
328
|
+
await sar.run(opts, new AbortController().signal);
|
|
329
|
+
|
|
330
|
+
expect(capturedOpts).toBeDefined();
|
|
331
|
+
expect(capturedOpts!.task).toBe("do the thing");
|
|
332
|
+
expect(capturedOpts!.agent).toBe("code-reviewer");
|
|
333
|
+
expect(capturedOpts!.skillPath).toBe("/skills/code-review.md");
|
|
334
|
+
// schemaEnv 应通过 ExecuteOptions 透传
|
|
335
|
+
expect((capturedOpts as Record<string, unknown>).schemaEnv).toBe('{"type":"object"}');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("schema 透传为原始对象", async () => {
|
|
339
|
+
let capturedOpts: Record<string, unknown> | undefined;
|
|
340
|
+
const mockService = createMockService(
|
|
341
|
+
vi.fn().mockImplementation((opts: Record<string, unknown>) => {
|
|
342
|
+
capturedOpts = opts;
|
|
343
|
+
return Promise.resolve(makeMockResult());
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
347
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
348
|
+
|
|
349
|
+
const opts = {
|
|
350
|
+
...makeBaseOpts(),
|
|
351
|
+
schema: { type: "object", properties: { name: { type: "string" } } },
|
|
352
|
+
};
|
|
353
|
+
await sar.run(opts, new AbortController().signal);
|
|
354
|
+
|
|
355
|
+
expect(capturedOpts!.schema).toEqual({
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: { name: { type: "string" } },
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ────────────────────────────────────────────────
|
|
363
|
+
// T3.18: executeAndAwait throw → catch → error
|
|
364
|
+
// ────────────────────────────────────────────────
|
|
365
|
+
describe("T3.18 异常处理", () => {
|
|
366
|
+
it("executeAndAwait throw → 不 reject,返回 error", async () => {
|
|
367
|
+
const mockService = createMockService(
|
|
368
|
+
vi.fn().mockRejectedValue(new Error("nesting depth exceeded")),
|
|
369
|
+
);
|
|
370
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
371
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
372
|
+
|
|
373
|
+
const result = await sar.run(makeBaseOpts(), new AbortController().signal);
|
|
374
|
+
expect(result.error).toContain("nesting depth exceeded");
|
|
375
|
+
expect(result.content).toBe("");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("非 Error throw → error 字段含 message", async () => {
|
|
379
|
+
const mockService = createMockService(
|
|
380
|
+
vi.fn().mockRejectedValue("raw string error"),
|
|
381
|
+
);
|
|
382
|
+
const deps: SubprocessAgentRunnerDeps = { subagentService: mockService };
|
|
383
|
+
const sar = new SubprocessAgentRunner(deps);
|
|
384
|
+
|
|
385
|
+
const result = await sar.run(makeBaseOpts(), new AbortController().signal);
|
|
386
|
+
expect(result.error).toBe("raw string error");
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/__tests__/temp-prompt.test.ts
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { cleanupTempPrompt, writePromptToTempFile } from "../temp-prompt.ts";
|
|
8
|
+
|
|
9
|
+
describe("writePromptToTempFile", () => {
|
|
10
|
+
it("创建临时目录 + 写入 prompt 文件", async () => {
|
|
11
|
+
const result = await writePromptToTempFile("test-agent", "You are a helper.");
|
|
12
|
+
expect(result.dir).toBeTruthy();
|
|
13
|
+
expect(result.filePath).toBeTruthy();
|
|
14
|
+
expect(fs.existsSync(result.dir)).toBe(true);
|
|
15
|
+
expect(fs.existsSync(result.filePath)).toBe(true);
|
|
16
|
+
|
|
17
|
+
const content = fs.readFileSync(result.filePath, "utf-8");
|
|
18
|
+
expect(content).toBe("You are a helper.");
|
|
19
|
+
|
|
20
|
+
// 清理
|
|
21
|
+
await cleanupTempPrompt(result);
|
|
22
|
+
expect(fs.existsSync(result.dir)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("agentName 非法字符替换为下划线", async () => {
|
|
26
|
+
const result = await writePromptToTempFile("my agent/v2:spec", "prompt");
|
|
27
|
+
expect(path.basename(result.filePath)).toBe("prompt-my_agent_v2_spec.md");
|
|
28
|
+
await cleanupTempPrompt(result);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("文件权限 0o600(仅 owner 读写)", async () => {
|
|
32
|
+
const result = await writePromptToTempFile("secure", "secret prompt");
|
|
33
|
+
const stat = fs.statSync(result.filePath);
|
|
34
|
+
// 只取低 9 位(rwxrwxrwx),忽略文件类型位
|
|
35
|
+
const mode = stat.mode & 0o777;
|
|
36
|
+
expect(mode).toBe(0o600);
|
|
37
|
+
await cleanupTempPrompt(result);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("多字节内容(中文)正确写入", async () => {
|
|
41
|
+
const result = await writePromptToTempFile("cn", "你是一个助手。");
|
|
42
|
+
const content = fs.readFileSync(result.filePath, "utf-8");
|
|
43
|
+
expect(content).toBe("你是一个助手。");
|
|
44
|
+
await cleanupTempPrompt(result);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("cleanupTempPrompt", () => {
|
|
49
|
+
it("不存在的目录不抛错(best-effort)", async () => {
|
|
50
|
+
const fake = { dir: "/nonexistent/path/xyz", filePath: "/nonexistent/path/xyz/p.md" };
|
|
51
|
+
await expect(cleanupTempPrompt(fake)).resolves.not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|