@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.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 注册层黑盒测试共享捕获 helper:fake pi 收集 registerTool 入参,断言恰好注册
3
- * 一个 tool(可锁定期望名)并返回首个。此前 7 个测试文件各自内联同构拷贝
4
- * (fake pi + tools 数组 + name 断言 + CapturedTool 类型),收敛至此单点;
5
- * 差异面(注册函数、pi 之后的实参、deps 形状)留在调用方闭包。
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 tools: unknown[] = [];
47
- register({ registerTool: (tool) => tools.push(tool) });
48
- const first = tools[0] as T | undefined;
49
- if (first === undefined || tools.length !== 1) {
50
- throw new Error(`expected exactly one registered tool, got ${tools.length}`);
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
- if (expectedName !== undefined && first.name !== expectedName) {
53
- throw new Error(`expected registered tool '${expectedName}', got '${String(first.name)}'`);
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 first;
76
+ return byName[names[0]] as T;
56
77
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * subagent tool 非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
3
- * 互补,覆盖 executeSubagent 的 action 路由全分支与 renderCall 的 model 预解析降级链)。
2
+ * subagent 工具组非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
3
+ * 互补,覆盖 subagent_manage / subagent_message 的 action 路由全分支、subagent_start
4
+ * 的顶层透传与 renderCall 的 model 预解析降级链)。
4
5
  *
5
- * 覆盖面:
6
- * - execute:service 缺席 → 带恢复动作的错误;未知 action → immediate throw;
7
- * list/cancel/message/close/fork-from 六通道 handler 接线 + adapter 包装
8
- * (content JSON 给 LLM / details 领域对象给 renderResult)
9
- * - renderCall:model override 三层解析(override 在场 → resolved 注入标题;
10
- * resolveModel 抛错 → 降级不崩;override 缺省 → resolved undefined)
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
- function captureTool(): SubagentToolView {
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
- captured = tool as SubagentToolView;
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("subagent tool not registered");
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("subagent tool execute:守卫与缺席面", () => {
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 = captureTool(); // 槽未注入
118
+ const tool = captureToolByName("subagent_start"); // 槽未注入
103
119
  await expect(
104
- tool.execute("c1", { action: "list" }, undefined, undefined, undefined),
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
- it("未知 action → immediate throw(Unknown subagent action)", async () => {
125
+ describe("subagent_manage execute:三 action 通道接线", () => {
126
+ /** 注入最小 fake service 并捕获 manage 工具(三通道共用基座)。 */
127
+ function withService(): SubagentToolView {
109
128
  setSubagentService({ execute: vi.fn() } as never);
110
- const tool = captureTool();
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("c2", { action: "pause" }, undefined, undefined, undefined),
113
- ).rejects.toThrow("Unknown subagent action: pause");
135
+ tool.execute("c0", { action: "list" }, undefined, undefined, undefined),
136
+ ).rejects.toThrow(/subagents runtime not initialized/);
114
137
  });
115
- });
116
138
 
117
- describe("subagent tool execute:六 action 通道接线", () => {
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
- return captureTool();
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("start:合法绝对路径放行,顶层 params 原样透传(含 signal/model override)", async () => {
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 = captureTool();
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("subagent tool renderCall:model 预解析", () => {
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 = captureTool();
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 = captureTool();
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 = captureTool(); // 槽空
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 = captureTool();
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
- // 守卫在 executeSubagent start 分支 immediate throw(与 action 枚举守卫同风格)。
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 后捕获 execute 回调直接调用。
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
- // executeSubagent 内 getSubagentService() 读同一槽,「守卫拒绝时 service.execute
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
- /** 注册 tool 并捕获 execute 回调(sdk-contract 同款)。 */
63
- function captureExecute(): ExecuteCb {
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
- captured = (tool as { execute: ExecuteCb }).execute;
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("subagent tool not registered");
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 { action: "start", task: "guard task", slug: "path-guard", ...over };
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("subagent tool 路径守卫(skillPath/cwd:绝对路径 + 禁 .. 穿越)", () => {
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(executeSubagent 内 getSubagentService 读同槽)
100
+ // 经真实单例访问器注入 fake service(executeSubagentStart 内 getSubagentService 读同槽)
97
101
  setSubagentService({ execute: mockServiceExecute, getCollectSyncDefault: () => "async" } as never);
98
- execute = captureExecute();
102
+ execute = captureStartExecute();
99
103
  });
100
104
 
101
105
  afterEach(() => {