@zhushanwen/pi-subagent-workflow 9.4.0 → 9.5.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 +19 -5
- package/package.json +10 -10
- package/src/host/pi-host.ts +9 -6
- package/src/index.ts +9 -2
- package/src/injectors/__tests__/workflow-list-injector.test.ts +11 -4
- package/src/injectors/resource-list-injector.ts +17 -7
- package/src/injectors/subagent-list-injector.ts +27 -2
- package/src/injectors/workflow-list-injector.ts +1 -1
- package/src/interface/command/commands.ts +126 -66
- package/src/interface/gui/subagent-actions.ts +4 -2
- package/src/interface/tool/__tests__/capture-tool.ts +34 -13
- package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +105 -41
- package/src/interface/tool/__tests__/subagent-tool-path-guard.test.ts +16 -12
- package/src/interface/tool/__tests__/tool-prompt-contract.test.ts +359 -175
- package/src/interface/tool/__tests__/tool-subagents.test.ts +28 -13
- package/src/interface/tool/__tests__/tool-workflow.test.ts +80 -59
- package/src/interface/tool/subagent-tool-schema.ts +79 -70
- package/src/interface/tool/subagent-tool.ts +205 -136
- package/src/interface/tool/tool-render.ts +7 -5
- package/src/interface/tool/tool-result.ts +3 -2
- package/src/interface/tool/tool-subagents.ts +39 -40
- package/src/interface/tool/tool-workflow-script.ts +7 -5
- package/src/interface/tool/tool-workflow.ts +164 -116
- package/src/jsonl-run-store.ts +126 -48
- package/src/session-lifecycle.ts +11 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 注册层黑盒测试共享捕获 helper:fake pi 收集 registerTool
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* 注册层黑盒测试共享捕获 helper:fake pi 收集 registerTool 入参,支持两种形态——
|
|
3
|
+
* ①恰好注册一个 tool(可锁定期望名);②一次注册多个工具(D3 拆分:registerSubagentTool
|
|
4
|
+
* 注册三工具、registerWorkflowTool 注册两工具)按名捕获。差异面(注册函数、pi 之后
|
|
5
|
+
* 的实参、deps 形状)留在调用方闭包。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/** 注册层捕获到的 tool 定义最小形态(各文件按需用泛型传窄 view 类型)。 */
|
|
@@ -32,25 +32,46 @@ export interface ScriptResultToolView extends CapturedTool { // oe-exempt:202609
|
|
|
32
32
|
}>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** 注册并捕获全部 tool 定义,按名索引(D3 拆分多工具注册形态)。 */
|
|
36
|
+
export function captureToolsByName(
|
|
37
|
+
register: (pi: { registerTool: (tool: unknown) => void }) => void,
|
|
38
|
+
): Record<string, CapturedTool> {
|
|
39
|
+
const tools: unknown[] = [];
|
|
40
|
+
register({ registerTool: (tool) => tools.push(tool) });
|
|
41
|
+
const byName: Record<string, CapturedTool> = {};
|
|
42
|
+
for (const tool of tools) {
|
|
43
|
+
const named = tool as CapturedTool;
|
|
44
|
+
byName[named.name] = named;
|
|
45
|
+
}
|
|
46
|
+
if (tools.length === 0) {
|
|
47
|
+
throw new Error("expected at least one registered tool, got 0");
|
|
48
|
+
}
|
|
49
|
+
return byName;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
/**
|
|
36
53
|
* 注册并捕获 tool 定义。泛型 T 供需要窄 execute 返回类型的调用方传 view
|
|
37
54
|
* 类型(方法参数双变 + 返回协变,窄形态天然 extends CapturedTool,零 cast)。
|
|
38
55
|
*
|
|
39
56
|
* @param register 拿 fake pi 执行注册(调用方闭包内传齐注册函数与其余实参)
|
|
40
|
-
* @param expectedName 期望的 tool
|
|
57
|
+
* @param expectedName 期望的 tool 名——给定:多工具注册中按名取出(D3 拆分形态);
|
|
58
|
+
* 缺省:断言恰好注册一个并返回首个(单工具注册形态)
|
|
41
59
|
*/
|
|
42
60
|
export function captureTool<T extends CapturedTool = CapturedTool>(
|
|
43
61
|
register: (pi: { registerTool: (tool: unknown) => void }) => void,
|
|
44
62
|
expectedName?: string,
|
|
45
63
|
): T {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
const byName = captureToolsByName(register);
|
|
65
|
+
if (expectedName !== undefined) {
|
|
66
|
+
const named = byName[expectedName] as T | undefined;
|
|
67
|
+
if (named === undefined) {
|
|
68
|
+
throw new Error(`expected registered tool '${expectedName}', got names: ${Object.keys(byName).join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
return named;
|
|
51
71
|
}
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
const names = Object.keys(byName);
|
|
73
|
+
if (names.length !== 1) {
|
|
74
|
+
throw new Error(`expected exactly one registered tool, got ${names.length}`);
|
|
54
75
|
}
|
|
55
|
-
return
|
|
76
|
+
return byName[names[0]] as T;
|
|
56
77
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent
|
|
3
|
-
* 互补,覆盖
|
|
2
|
+
* subagent 工具组非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
|
|
3
|
+
* 互补,覆盖 subagent_manage / subagent_message 的 action 路由全分支、subagent_start
|
|
4
|
+
* 的顶层透传与 renderCall 的 model 预解析降级链)。
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
6
|
+
* 覆盖面(tool-declaration-slimming D3 拆分后):
|
|
7
|
+
* - subagent_start:service 缺席 → 带恢复动作的错误;顶层 params 原样透传
|
|
8
|
+
* - subagent_manage:list/cancel/close 三通道 handler 接线 + adapter 包装;
|
|
9
|
+
* 未知 action → immediate throw
|
|
10
|
+
* - subagent_message:message/fork-from 两通道接线;未知 action → immediate throw
|
|
11
|
+
* - renderCall(subagent_start):model override 三层解析(override 在场 → resolved
|
|
12
|
+
* 注入标题;resolveModel 抛错 → 降级不崩;override 缺省 → resolved undefined)
|
|
11
13
|
*
|
|
12
14
|
* mock 策略:六 handler 深路径 stub(领域内核行为由 core 侧
|
|
13
15
|
* subagent-actions-core.test.ts 锁定,此处只测接线);pi-ai/typebox 用共享桩
|
|
@@ -65,15 +67,18 @@ interface SubagentToolView { // oe-exempt:20260930:test:test double shape for to
|
|
|
65
67
|
renderCall: (args: unknown, theme: unknown, ctx: unknown) => { render(width: number): string[] };
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
/** 按名捕获注册的 tool(D3 拆分:一次注册三个)。 */
|
|
71
|
+
function captureToolByName(toolName: string): SubagentToolView {
|
|
69
72
|
let captured: SubagentToolView | undefined;
|
|
70
73
|
const pi = mockExtensionApi({
|
|
71
74
|
registerTool: (tool: unknown) => {
|
|
72
|
-
|
|
75
|
+
if ((tool as { name?: string }).name === toolName) {
|
|
76
|
+
captured = tool as SubagentToolView;
|
|
77
|
+
}
|
|
73
78
|
},
|
|
74
79
|
});
|
|
75
80
|
registerSubagentTool(pi);
|
|
76
|
-
if (!captured) throw new Error(
|
|
81
|
+
if (!captured) throw new Error(`${toolName} tool not registered`);
|
|
77
82
|
return captured;
|
|
78
83
|
}
|
|
79
84
|
|
|
@@ -97,29 +102,47 @@ afterEach(() => {
|
|
|
97
102
|
resetServiceSlot();
|
|
98
103
|
});
|
|
99
104
|
|
|
100
|
-
describe("
|
|
105
|
+
describe("注册面:三工具名(D3 拆分终态)", () => {
|
|
106
|
+
it("registerSubagentTool 注册 subagent_start / subagent_manage / subagent_message 三工具", () => {
|
|
107
|
+
const names: string[] = [];
|
|
108
|
+
const pi = mockExtensionApi({
|
|
109
|
+
registerTool: (tool: unknown) => names.push((tool as { name: string }).name),
|
|
110
|
+
});
|
|
111
|
+
registerSubagentTool(pi);
|
|
112
|
+
expect(names.sort()).toEqual(["subagent_manage", "subagent_message", "subagent_start"]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("subagent_start execute:守卫与缺席面", () => {
|
|
101
117
|
it("service 缺席 → immediate throw,错误带恢复动作指引", async () => {
|
|
102
|
-
const tool =
|
|
118
|
+
const tool = captureToolByName("subagent_start"); // 槽未注入
|
|
103
119
|
await expect(
|
|
104
|
-
tool.execute("c1", {
|
|
120
|
+
tool.execute("c1", { task: "t", slug: "s" }, undefined, undefined, undefined),
|
|
105
121
|
).rejects.toThrow(/subagents runtime not initialized.*Recovery: reload this session/s);
|
|
106
122
|
});
|
|
123
|
+
});
|
|
107
124
|
|
|
108
|
-
|
|
125
|
+
describe("subagent_manage execute:三 action 通道接线", () => {
|
|
126
|
+
/** 注入最小 fake service 并捕获 manage 工具(三通道共用基座)。 */
|
|
127
|
+
function withService(): SubagentToolView {
|
|
109
128
|
setSubagentService({ execute: vi.fn() } as never);
|
|
110
|
-
|
|
129
|
+
return captureToolByName("subagent_manage");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
it("service 缺席 → immediate throw(manage 同文案单源)", async () => {
|
|
133
|
+
const tool = captureToolByName("subagent_manage");
|
|
111
134
|
await expect(
|
|
112
|
-
tool.execute("
|
|
113
|
-
).rejects.toThrow(
|
|
135
|
+
tool.execute("c0", { action: "list" }, undefined, undefined, undefined),
|
|
136
|
+
).rejects.toThrow(/subagents runtime not initialized/);
|
|
114
137
|
});
|
|
115
|
-
});
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
/** 注入最小 fake service 并捕获 tool(六通道共用基座)。 */
|
|
119
|
-
function withService(): SubagentToolView {
|
|
139
|
+
it("未知 action → immediate throw(Unknown subagent_manage action)", async () => {
|
|
120
140
|
setSubagentService({ execute: vi.fn() } as never);
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const tool = withService();
|
|
142
|
+
await expect(
|
|
143
|
+
tool.execute("c2", { action: "pause" }, undefined, undefined, undefined),
|
|
144
|
+
).rejects.toThrow("Unknown subagent_manage action: pause");
|
|
145
|
+
});
|
|
123
146
|
|
|
124
147
|
it("list:listParam 解包透传,adapter 包装 listResponse", async () => {
|
|
125
148
|
vi.mocked(listHandler).mockReturnValue({ response: { items: [], total: 0 } } as never);
|
|
@@ -140,15 +163,6 @@ describe("subagent tool execute:六 action 通道接线", () => {
|
|
|
140
163
|
expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "cancel", subagentId: "sa-9" });
|
|
141
164
|
});
|
|
142
165
|
|
|
143
|
-
it("message:messageParam 解包透传", async () => {
|
|
144
|
-
vi.mocked(messageHandler).mockResolvedValue({ subagentId: "sa-7", response: { queued: true } } as never);
|
|
145
|
-
const tool = withService();
|
|
146
|
-
const params = { action: "message", messageParam: { subagentId: "sa-7", text: "continue" } };
|
|
147
|
-
const result = await tool.execute("c5", params, undefined, undefined, undefined);
|
|
148
|
-
expect(vi.mocked(messageHandler)).toHaveBeenCalledWith(expect.anything(), params.messageParam);
|
|
149
|
-
expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "message", subagentId: "sa-7" });
|
|
150
|
-
});
|
|
151
|
-
|
|
152
166
|
it("close:closeParam 解包透传", async () => {
|
|
153
167
|
vi.mocked(closeHandler).mockResolvedValue({ subagentId: "sa-6", response: { archived: true } } as never);
|
|
154
168
|
const tool = withService();
|
|
@@ -157,6 +171,23 @@ describe("subagent tool execute:六 action 通道接线", () => {
|
|
|
157
171
|
expect(vi.mocked(closeHandler)).toHaveBeenCalledWith(expect.anything(), { subagentId: "sa-6" });
|
|
158
172
|
expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "close", subagentId: "sa-6" });
|
|
159
173
|
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("subagent_message execute:两 action 通道接线", () => {
|
|
177
|
+
/** 注入最小 fake service 并捕获 message 工具(两通道共用基座)。 */
|
|
178
|
+
function withService(): SubagentToolView {
|
|
179
|
+
setSubagentService({ execute: vi.fn() } as never);
|
|
180
|
+
return captureToolByName("subagent_message");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
it("message:messageParam 解包透传", async () => {
|
|
184
|
+
vi.mocked(messageHandler).mockResolvedValue({ subagentId: "sa-7", response: { queued: true } } as never);
|
|
185
|
+
const tool = withService();
|
|
186
|
+
const params = { action: "message", messageParam: { subagentId: "sa-7", text: "continue" } };
|
|
187
|
+
const result = await tool.execute("c5", params, undefined, undefined, undefined);
|
|
188
|
+
expect(vi.mocked(messageHandler)).toHaveBeenCalledWith(expect.anything(), params.messageParam);
|
|
189
|
+
expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "message", subagentId: "sa-7" });
|
|
190
|
+
});
|
|
160
191
|
|
|
161
192
|
it("fork-from:forkFromParam 解包透传", async () => {
|
|
162
193
|
vi.mocked(forkFromHandler).mockResolvedValue({
|
|
@@ -170,7 +201,17 @@ describe("subagent tool execute:六 action 通道接线", () => {
|
|
|
170
201
|
expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "fork-from", subagentId: "sa-new" });
|
|
171
202
|
});
|
|
172
203
|
|
|
173
|
-
it("
|
|
204
|
+
it("未知 action → immediate throw(Unknown subagent_message action)", async () => {
|
|
205
|
+
setSubagentService({ execute: vi.fn() } as never);
|
|
206
|
+
const tool = withService();
|
|
207
|
+
await expect(
|
|
208
|
+
tool.execute("c2m", { action: "pause" }, undefined, undefined, undefined),
|
|
209
|
+
).rejects.toThrow("Unknown subagent_message action: pause");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("subagent_start execute:顶层透传", () => {
|
|
214
|
+
it("start:合法绝对路径放行,顶层 params 原样透传(无 action 字段;signal/model ctx 透传)", async () => {
|
|
174
215
|
vi.mocked(startHandler).mockResolvedValue({
|
|
175
216
|
subagentId: "sa-1",
|
|
176
217
|
slug: "demo",
|
|
@@ -178,9 +219,8 @@ describe("subagent tool execute:六 action 通道接线", () => {
|
|
|
178
219
|
response: { mode: "background" },
|
|
179
220
|
} as never);
|
|
180
221
|
setSubagentService({ execute: vi.fn() } as never);
|
|
181
|
-
const tool =
|
|
222
|
+
const tool = captureToolByName("subagent_start");
|
|
182
223
|
const params = {
|
|
183
|
-
action: "start",
|
|
184
224
|
task: "t",
|
|
185
225
|
slug: "demo",
|
|
186
226
|
skillPath: "/abs/skills/x.md",
|
|
@@ -193,12 +233,12 @@ describe("subagent tool execute:六 action 通道接线", () => {
|
|
|
193
233
|
});
|
|
194
234
|
});
|
|
195
235
|
|
|
196
|
-
describe("
|
|
236
|
+
describe("subagent_start renderCall:model 预解析", () => {
|
|
197
237
|
it("override 在场 + resolveModel 成功 → resolved 注入标题(provider/model · thinking)", () => {
|
|
198
238
|
setSubagentService({
|
|
199
239
|
resolveModel: () => ({ model: { provider: "p", id: "m1" }, thinkingLevel: "high" }),
|
|
200
240
|
} as never);
|
|
201
|
-
const tool =
|
|
241
|
+
const tool = captureToolByName("subagent_start");
|
|
202
242
|
const comp = tool.renderCall({ agent: "coder", model: "p/m1", thinkingLevel: "high", slug: "demo" }, markingTheme, {});
|
|
203
243
|
const text = comp.render(200).join("");
|
|
204
244
|
expect(text).toContain("p/m1");
|
|
@@ -212,7 +252,7 @@ describe("subagent tool renderCall:model 预解析", () => {
|
|
|
212
252
|
throw new Error("registry cold");
|
|
213
253
|
},
|
|
214
254
|
} as never);
|
|
215
|
-
const tool =
|
|
255
|
+
const tool = captureToolByName("subagent_start");
|
|
216
256
|
const comp = tool.renderCall({ agent: "coder", model: "p/m1" }, markingTheme, {});
|
|
217
257
|
const text = comp.render(80).join("");
|
|
218
258
|
expect(text).toContain("subagent");
|
|
@@ -220,7 +260,7 @@ describe("subagent tool renderCall:model 预解析", () => {
|
|
|
220
260
|
});
|
|
221
261
|
|
|
222
262
|
it("service 缺席 → resolved undefined 降级;无 agent 字段兜底默认名", () => {
|
|
223
|
-
const tool =
|
|
263
|
+
const tool = captureToolByName("subagent_start"); // 槽空
|
|
224
264
|
const comp = tool.renderCall({ task: "t" }, markingTheme, {});
|
|
225
265
|
const text = comp.render(80).join("");
|
|
226
266
|
expect(text).toContain("subagent");
|
|
@@ -231,8 +271,32 @@ describe("subagent tool renderCall:model 预解析", () => {
|
|
|
231
271
|
setSubagentService({
|
|
232
272
|
resolveModel: () => ({ model: { provider: "p", id: "m1" }, thinkingLevel: "high" }),
|
|
233
273
|
} as never);
|
|
234
|
-
const tool =
|
|
274
|
+
const tool = captureToolByName("subagent_start");
|
|
235
275
|
const comp = tool.renderCall(null, markingTheme, {});
|
|
236
276
|
expect(comp.render(80).join("")).toContain("subagent");
|
|
237
277
|
});
|
|
238
278
|
});
|
|
279
|
+
|
|
280
|
+
describe("subagent_manage / subagent_message renderCall:轻量标题行", () => {
|
|
281
|
+
it("manage:标题含工具名与 action", () => {
|
|
282
|
+
const tool = captureToolByName("subagent_manage");
|
|
283
|
+
const comp = tool.renderCall({ action: "list", listParam: {} }, markingTheme, {});
|
|
284
|
+
const text = comp.render(200).join("");
|
|
285
|
+
expect(text).toContain("subagent_manage");
|
|
286
|
+
expect(text).toContain("list");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("message:标题含工具名与 action", () => {
|
|
290
|
+
const tool = captureToolByName("subagent_message");
|
|
291
|
+
const comp = tool.renderCall({ action: "fork-from", forkFromParam: {} }, markingTheme, {});
|
|
292
|
+
const text = comp.render(200).join("");
|
|
293
|
+
expect(text).toContain("subagent_message");
|
|
294
|
+
expect(text).toContain("fork-from");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("args 缺 action → 不崩,标题仍渲染工具名", () => {
|
|
298
|
+
const tool = captureToolByName("subagent_manage");
|
|
299
|
+
const comp = tool.renderCall(null, markingTheme, {});
|
|
300
|
+
expect(comp.render(200).join("")).toContain("subagent_manage");
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
// 闸。schema pattern(^/)经 pi agent-loop 运行时强校验已是强制(PS-20:
|
|
6
6
|
// agent-loop.js:403-404 → validation.js:247-273),工具层守卫定位 = defense-in-depth
|
|
7
7
|
// + schema 表达力缺口——`..` 穿越语义超出 pattern 能力(^/ 放行 "/a/../b"),
|
|
8
|
-
// 守卫在
|
|
8
|
+
// 守卫在 executeSubagentStart immediate throw(与 action 枚举守卫同风格)。
|
|
9
9
|
//
|
|
10
10
|
// 本文件锁住:
|
|
11
11
|
// 1. skillPath / cwd 含 `..` 穿越段 → 同步 reject,service.execute 零触达
|
|
12
12
|
// 2. skillPath / cwd 相对路径 → 同步 reject
|
|
13
13
|
// 3. 合法绝对路径放行 → 参数原样透传 service.execute(守卫零误伤对照)
|
|
14
14
|
//
|
|
15
|
-
// harness([u-5b / A-V3] 改写):registerSubagentTool
|
|
15
|
+
// harness([u-5b / A-V3] 改写):registerSubagentTool 后按名捕获 subagent_start 的
|
|
16
|
+
// execute 回调直接调用(D3 拆分:registerSubagentTool 注册三工具,start 守卫在
|
|
17
|
+
// subagent_start 的 execute)。
|
|
16
18
|
// SubagentService 经单例访问器槽注入 fake(setSubagentService,globalThis 槽——
|
|
17
|
-
//
|
|
19
|
+
// executeSubagentStart 内 getSubagentService() 读同一槽,「守卫拒绝时 service.execute
|
|
18
20
|
// 零触达」的承重断言不依赖整类 mock);pi-ai/typebox 桩改共享桩
|
|
19
21
|
// (src/__tests__/mocks/runtime-stubs.ts,async 工厂 + 动态 import,消费约定见
|
|
20
22
|
// 桩 module 头注)。
|
|
@@ -59,22 +61,24 @@ function resetServiceSlot(): void {
|
|
|
59
61
|
|
|
60
62
|
type ExecuteCb = (...args: unknown[]) => Promise<unknown>;
|
|
61
63
|
|
|
62
|
-
/**
|
|
63
|
-
function
|
|
64
|
+
/** 注册工具组并按名捕获 subagent_start 的 execute 回调。 */
|
|
65
|
+
function captureStartExecute(): ExecuteCb {
|
|
64
66
|
let captured: ExecuteCb | undefined;
|
|
65
67
|
const pi = mockExtensionApi({
|
|
66
68
|
registerTool: (tool: unknown) => {
|
|
67
|
-
|
|
69
|
+
if ((tool as { name?: string }).name === "subagent_start") {
|
|
70
|
+
captured = (tool as { execute: ExecuteCb }).execute;
|
|
71
|
+
}
|
|
68
72
|
},
|
|
69
73
|
});
|
|
70
74
|
registerSubagentTool(pi);
|
|
71
|
-
if (!captured) throw new Error("
|
|
75
|
+
if (!captured) throw new Error("subagent_start tool not registered");
|
|
72
76
|
return captured;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
/** 合法 start 入参基线(守卫测试只变动 skillPath/cwd 字段)。 */
|
|
79
|
+
/** 合法 start 入参基线(守卫测试只变动 skillPath/cwd 字段;start 工具无 action 字段)。 */
|
|
76
80
|
function baseParams(over: Record<string, unknown> = {}): Record<string, unknown> {
|
|
77
|
-
return {
|
|
81
|
+
return { task: "guard task", slug: "path-guard", ...over };
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/** 合法 execute 返回值 stub(放行路径 adapter 包装用)。 */
|
|
@@ -87,15 +91,15 @@ function stubHandle() {
|
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
describe("
|
|
94
|
+
describe("subagent_start 路径守卫(skillPath/cwd:绝对路径 + 禁 .. 穿越)", () => {
|
|
91
95
|
let execute: ExecuteCb;
|
|
92
96
|
|
|
93
97
|
beforeEach(() => {
|
|
94
98
|
vi.clearAllMocks();
|
|
95
99
|
mockServiceExecute.mockResolvedValue(stubHandle());
|
|
96
|
-
// 经真实单例访问器注入 fake service(
|
|
100
|
+
// 经真实单例访问器注入 fake service(executeSubagentStart 内 getSubagentService 读同槽)
|
|
97
101
|
setSubagentService({ execute: mockServiceExecute, getCollectSyncDefault: () => "async" } as never);
|
|
98
|
-
execute =
|
|
102
|
+
execute = captureStartExecute();
|
|
99
103
|
});
|
|
100
104
|
|
|
101
105
|
afterEach(() => {
|