@zhushanwen/pi-subagent-workflow 9.4.1 → 9.5.1

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,4 +1,4 @@
1
- // src/interface/__tests__/tool-subagents.test.ts
1
+ // src/interface/tool/__tests__/tool-subagents.test.ts
2
2
  //
3
3
  // u2 契约测试:`subagents` 批量 tool(schema 平铺 D2 / handler 转译 D3 / 错误规格 D9 /
4
4
  // 集合收录 D1 / 渲染不构造 __gui__ D8)。
@@ -38,6 +38,11 @@ import { captureTool } from "./capture-tool.ts";
38
38
 
39
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
40
40
  const TOOL_SRC = readFileSync(join(__dirname, "../tool-subagents.ts"), "utf-8");
41
+ // 跨工具通用纪律的收敛点(tool-declaration-slimming D1 单源化终态的注入段侧锚源)。
42
+ const SUBAGENT_LIST_INJECTOR_SRC = readFileSync(
43
+ join(__dirname, "../../../injectors/subagent-list-injector.ts"),
44
+ "utf-8",
45
+ );
41
46
 
42
47
  // ── 注册面最小类型(单层 cast:目标类型全必填,非 no-unsafe-cast 命中的全可选结构断言)──
43
48
 
@@ -180,9 +185,9 @@ describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
180
185
  expect(tool.label.length).toBeGreaterThan(0);
181
186
  expect(tool.description).toContain("one-shot batch member");
182
187
  expect(tool.description).toContain("ONE notification");
183
- // 分工句(D5①):单数 tool 指路 + 依赖链场景指路 workflow
184
- expect(tool.description).toContain("`subagent` tool");
185
- expect(tool.description).toContain("`workflow` tool");
188
+ // 分工句(D5① + D3 拆分新名):单数续聊指路 subagent_message + 依赖链场景指路 workflow_run
189
+ expect(tool.description).toContain("`subagent_message` tool");
190
+ expect(tool.description).toContain("`workflow_run` tool");
186
191
  });
187
192
 
188
193
  it("参数全平铺:无 action 分发、无 args 嵌套、无嵌套 object 参数", () => {
@@ -228,13 +233,24 @@ describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
228
233
  expect(TOOL_SRC).not.toContain("isGuiCapable");
229
234
  });
230
235
 
231
- it("promptGuidelines 含分工句/调用正例/预算默认(弱模型结构信号依赖面)", () => {
236
+ it("promptGuidelines 含分工句/调用正例;跨工具纪律单源化零残留(D1)", () => {
232
237
  const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
233
238
  const guidelines = tool.promptGuidelines.join("\n");
234
239
  expect(guidelines).toContain("2+ independent tasks in one dispatch");
235
240
  expect(guidelines).toContain('{"tasks":[');
236
- expect(guidelines).toContain("Do NOT set tokens/time unless the user explicitly requests");
237
- expect(guidelines).toContain("workflow tool");
241
+ expect(guidelines).toContain("`workflow_run` tool");
242
+ expect(guidelines).toContain("`workflow_control` tool");
243
+ // tool-declaration-slimming D1 单源化终态:勿等待/预算/模型缺省继承三类跨工具
244
+ // 通用纪律不在本工具 promptGuidelines 重复——收敛到 <available_subagents> 注入段
245
+ // (subagent-list-injector.ts SUBAGENT_DISPATCH_SHARED_RULES)说一次。
246
+ expect(guidelines).not.toContain("Do NOT poll after starting");
247
+ expect(guidelines).not.toContain("Do NOT set tokens/time unless the user explicitly requests");
248
+ expect(guidelines).not.toContain("Model/thinkingLevel: omit by default");
249
+ expect(guidelines).not.toContain("Omit to inherit the main agent's model");
250
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("SUBAGENT_DISPATCH_SHARED_RULES");
251
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("busy-wait, or poll");
252
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("explicitly requests a limit");
253
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("inherit the main agent's model");
238
254
  });
239
255
  });
240
256
 
@@ -304,11 +320,10 @@ describe("handler 组装:runWorkflow('fan-out') 确定性映射(D3)", () =
304
320
  const text = result.content[0].text;
305
321
  expect(text).toContain("Started batch 'foo-tri-review' (wf-1700000000000-abcd) as workflow run 'fan-out' — 2 subagents dispatched in parallel (allSettled).");
306
322
  expect(text).toContain("Results arrive as ONE notification when the run settles. Do NOT poll.");
307
- expect(text).toContain("make a SINGLE status check: workflow tool with runId wf-1700000000000-abcd (recovery exit, not a poll loop).");
308
- expect(text).toContain("To abort: workflow tool, action abort, runId wf-1700000000000-abcd");
309
- // status/abort 不复制:本 tool 无 action 参数(schema 已断言),文案只指路 workflow tool
310
- expect(text).not.toContain('"action":"status"');
311
- expect(text).not.toContain('"action":"abort"');
323
+ expect(text).toContain('make a SINGLE status check: workflow_control {"action":"status"} with runId wf-1700000000000-abcd (recovery exit, not a poll loop).');
324
+ expect(text).toContain('To abort: workflow_control {"action":"abort","runId":"wf-1700000000000-abcd"}');
325
+ // status/abort 不复制:本 tool 无 action 参数(schema 已断言),文案只指路
326
+ // workflow_control 的 action 调用形态(D3 拆分新名)
312
327
  });
313
328
  });
314
329
 
@@ -410,7 +425,7 @@ describe("错误规格(D9)", () => {
410
425
  });
411
426
 
412
427
  // ══════════════════════════════════════════════════════════════
413
- describe("reentry guard:与 workflow tool 共用同一实例", () => {
428
+ describe("reentry guard:与 workflow_run/workflow_control 共用同一实例", () => {
414
429
  it("guard 被占用 → throw busy 文案,且不触达 runWorkflow", async () => {
415
430
  const tool = captureSubagentsTool({ isProcessing: true }, makeRegistry());
416
431
  await expect(
@@ -145,7 +145,7 @@ interface StatusToolView extends CapturedTool { // oe-exempt:20260927:wip:test f
145
145
 
146
146
  /** 注册层黑盒 capture(actionStatus/actionAbort 未导出,经 execute 唯一入口;
147
147
  * fake pi 捕获单点见 capture-tool.ts,此处只留差异面:runs Map + 泛型 view)。 */
148
- function captureRegisteredTool<T extends CapturedTool>(runs: Map<string, WorkflowRun>): T {
148
+ function captureRegisteredTool<T extends CapturedTool>(runs: Map<string, WorkflowRun>, toolName: string): T {
149
149
  const deps = {
150
150
  runs,
151
151
  // status/abort 路径只触 store.stateFilePath(RunSummary 宿主扩展字段)
@@ -155,7 +155,7 @@ function captureRegisteredTool<T extends CapturedTool>(runs: Map<string, Workflo
155
155
  const guard: ReentryGuardRef = { isProcessing: false };
156
156
  return captureTool<T>(
157
157
  (pi) => registerWorkflowTool(pi as never, deps as never, guard),
158
- "workflow",
158
+ toolName,
159
159
  );
160
160
  }
161
161
 
@@ -214,7 +214,7 @@ describe("actionStatus 输出形态(LLM 可见文本锁)", () => {
214
214
  });
215
215
 
216
216
  it("runs 为空 → 固定空态文案 + 空 runs details", async () => {
217
- const tool = captureRegisteredTool<StatusToolView>(new Map());
217
+ const tool = captureRegisteredTool<StatusToolView>(new Map(), "workflow_control");
218
218
  const r = await tool.execute("id", { action: "status" }, undefined, undefined, {});
219
219
  expect(r.content).toEqual([{ type: "text", text: "No workflows in current session." }]);
220
220
  expect(r.details).toMatchObject({ action: "status", runs: [] });
@@ -249,7 +249,7 @@ describe("actionStatus 输出形态(LLM 可见文本锁)", () => {
249
249
  [doneFailed.runId, doneFailed],
250
250
  [doneCompleted.runId, doneCompleted],
251
251
  ]);
252
- const tool = captureRegisteredTool<StatusToolView>(runs);
252
+ const tool = captureRegisteredTool<StatusToolView>(runs, "workflow_control");
253
253
  const r = await tool.execute("id", { action: "status" }, undefined, undefined, {});
254
254
 
255
255
  expect(r.content).toEqual([
@@ -297,7 +297,7 @@ describe("actionStatus 输出形态(LLM 可见文本锁)", () => {
297
297
 
298
298
  describe("details 形状(execute 级无 GUI attach)", () => {
299
299
  it("RPC ctx → details 不含 __gui__ 键(workflow tool 不构造 GUI 描述符)", async () => {
300
- const tool = captureRegisteredTool<Record<string, unknown>>(new Map());
300
+ const tool = captureRegisteredTool<Record<string, unknown>>(new Map(), "workflow_control");
301
301
  const r = await tool.execute("id", { action: "status" }, undefined, undefined, {
302
302
  mode: "rpc",
303
303
  hasUI: true,
@@ -310,11 +310,12 @@ describe("details 形状(execute 级无 GUI attach)", () => {
310
310
  // W4b 错误路径 throw 语义(原 tool-workflow-throw-paths.test.ts)
311
311
  // ══════════════════════════════════════════════════════════════
312
312
 
313
- /** throw-paths 专用 capture wrapper(差异面:deps 实参 + reentry guard 可注入占用态)。 */
314
- function captureWorkflowTool(deps: unknown, reentryRef: ReentryGuardRef): CapturedTool {
313
+ /** throw-paths 专用 capture wrapper(差异面:deps 实参 + reentry guard 可注入占用态;
314
+ * D3 拆分后按 toolName 取 workflow_run / workflow_control)。 */
315
+ function captureWorkflowTool(deps: unknown, reentryRef: ReentryGuardRef, toolName: string): CapturedTool {
315
316
  return captureTool(
316
317
  (pi) => registerWorkflowTool(pi as never, deps as never, reentryRef),
317
- "workflow",
318
+ toolName,
318
319
  );
319
320
  }
320
321
 
@@ -327,7 +328,7 @@ function scriptRegistry(parameters?: object): Record<string, unknown> {
327
328
 
328
329
  describe("W4b: workflow tool 错误路径 throw 语义", () => {
329
330
  it("not_found:abort 目标 runId 不存在 → throw(pi catch 后置 isError:true)", async () => {
330
- const tool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false });
331
+ const tool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false }, "workflow_control");
331
332
  await expect(
332
333
  tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
333
334
  ).rejects.toThrow(
@@ -351,9 +352,9 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
351
352
  }),
352
353
  }),
353
354
  );
354
- const tool = captureWorkflowTool(deps, { isProcessing: false });
355
+ const tool = captureWorkflowTool(deps, { isProcessing: false }, "workflow_run");
355
356
  await expect(
356
- tool.execute("id", { action: "run", name: "/tmp/no-such-workflow.js" }, undefined, undefined, {}),
357
+ tool.execute("id", { name: "/tmp/no-such-workflow.js" }, undefined, undefined, {}),
357
358
  ).rejects.toThrow("Workflow '/tmp/no-such-workflow.js' not found.");
358
359
  // 未假启动:runs 注册表不出现该 run
359
360
  expect(deps.runs).toBeInstanceOf(Map);
@@ -362,7 +363,7 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
362
363
 
363
364
  it("reentry-busy:guard 占用 → throw REENTRY_BUSY_MESSAGE,且 guard 状态不被污染", async () => {
364
365
  const guard: ReentryGuardRef = { isProcessing: true };
365
- const tool = captureWorkflowTool(makeDeps(stubRegistry()), guard);
366
+ const tool = captureWorkflowTool(makeDeps(stubRegistry()), guard, "workflow_control");
366
367
  await expect(
367
368
  tool.execute("id", { action: "status" }, undefined, undefined, {}),
368
369
  ).rejects.toThrow(REENTRY_BUSY_MESSAGE);
@@ -378,17 +379,17 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
378
379
  required: ["task"],
379
380
  }),
380
381
  );
381
- const tool = captureWorkflowTool(deps, { isProcessing: false });
382
+ const tool = captureWorkflowTool(deps, { isProcessing: false }, "workflow_run");
382
383
  await expect(
383
384
  tool.execute(
384
385
  "id",
385
- { action: "run", name: "/abs/demo-wf.js", task: "do work" },
386
+ { name: "/abs/demo-wf.js", task: "do work" },
386
387
  undefined,
387
388
  undefined,
388
389
  {},
389
390
  ),
390
391
  ).rejects.toThrow(
391
- /Detected task at top level — they belong inside 'args'\. Correct: \{"action":"run","name":"\/abs\/demo-wf\.js","args":\{"task": "<value>"\}\}/,
392
+ /Detected task at top level — they belong inside 'args'\. Correct: \{"name":"\/abs\/demo-wf\.js","args":\{"task": "<value>"\}\}/,
392
393
  );
393
394
  });
394
395
 
@@ -400,12 +401,12 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
400
401
  required: ["task"],
401
402
  }),
402
403
  );
403
- const tool = captureWorkflowTool(deps, { isProcessing: false });
404
+ const tool = captureWorkflowTool(deps, { isProcessing: false }, "workflow_run");
404
405
  const longSlug = "a".repeat(SLUG_MAX_LENGTH + 1);
405
406
  await expect(
406
407
  tool.execute(
407
408
  "id",
408
- { action: "run", name: "/abs/demo-wf.js", slug: longSlug, args: {} },
409
+ { name: "/abs/demo-wf.js", slug: longSlug, args: {} },
409
410
  undefined,
410
411
  undefined,
411
412
  {},
@@ -423,13 +424,13 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
423
424
  required: ["task"],
424
425
  }),
425
426
  );
426
- const tool = captureWorkflowTool(deps, { isProcessing: false });
427
+ const tool = captureWorkflowTool(deps, { isProcessing: false }, "workflow_run");
427
428
  // LLM 对「跑久一点」完全可能生成 1e12——超 2^31-1 的典型形态(schema Type.Number
428
429
  // 直通无上界,用户入口 fail-fast 不依赖 lifecycle 内层 assertSafeTimerDelay)
429
430
  await expect(
430
431
  tool.execute(
431
432
  "id",
432
- { action: "run", name: "/abs/demo-wf.js", args: { task: "do work" }, time: 1_000_000_000_000 },
433
+ { name: "/abs/demo-wf.js", args: { task: "do work" }, time: 1_000_000_000_000 },
433
434
  undefined,
434
435
  undefined,
435
436
  {},
@@ -454,11 +455,11 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
454
455
  required: ["task"],
455
456
  }),
456
457
  );
457
- const tool = captureWorkflowTool(deps, { isProcessing: false });
458
+ const tool = captureWorkflowTool(deps, { isProcessing: false }, "workflow_run");
458
459
  await expect(
459
460
  tool.execute(
460
461
  "id",
461
- { action: "run", name: "/abs/demo-wf.js", args: { task: "do work" }, ...params },
462
+ { name: "/abs/demo-wf.js", args: { task: "do work" }, ...params },
462
463
  undefined,
463
464
  undefined,
464
465
  {},
@@ -472,7 +473,7 @@ describe("W4b: workflow tool 错误路径 throw 语义", () => {
472
473
  it("throw 后 reentry guard 经 finally 正常释放(成功路径回归)", async () => {
473
474
  // abort not_found throw 穿透 execute try/finally:guard 必须复位,否则后续命令全部 busy
474
475
  const guard: ReentryGuardRef = { isProcessing: false };
475
- const tool = captureWorkflowTool(makeDeps(stubRegistry()), guard);
476
+ const tool = captureWorkflowTool(makeDeps(stubRegistry()), guard, "workflow_control");
476
477
  await expect(
477
478
  tool.execute("id", { action: "abort", runId: "no-such-run" }, undefined, undefined, {}),
478
479
  ).rejects.toThrow(/not found/);
@@ -509,7 +510,7 @@ describe("D8 创建期模型拒单", () => {
509
510
  // setModelConfigService 无复位面,单例 set 后 null 分支不可再达。
510
511
  const deps = depsWithScripts([SCRIPT]);
511
512
  await actionRun(
512
- { action: "run", name: "/abs/chain.js", args: {}, model: "prov1/typo" } as never,
513
+ { name: "/abs/chain.js", args: {}, model: "prov1/typo" } as never,
513
514
  deps as never,
514
515
  undefined,
515
516
  );
@@ -523,7 +524,7 @@ describe("D8 创建期模型拒单", () => {
523
524
  ]);
524
525
  const deps = depsWithScripts([SCRIPT]);
525
526
  const err = await actionRun(
526
- { action: "run", name: "/abs/chain.js", args: {}, model: "prov1/typo" } as never,
527
+ { name: "/abs/chain.js", args: {}, model: "prov1/typo" } as never,
527
528
  deps as never,
528
529
  undefined,
529
530
  ).catch((e: unknown) => e as Error);
@@ -541,7 +542,7 @@ describe("D8 创建期模型拒单", () => {
541
542
  setCatalogService([{ provider: "prov1", id: "good-a" }]);
542
543
  const deps = depsWithScripts([SCRIPT]);
543
544
  const err = await actionRun(
544
- { action: "run", name: "/abs/chain.js", args: {}, model: "retired/m1" } as never,
545
+ { name: "/abs/chain.js", args: {}, model: "retired/m1" } as never,
545
546
  deps as never,
546
547
  undefined,
547
548
  ).catch((e: unknown) => e as Error);
@@ -556,7 +557,7 @@ describe("D8 创建期模型拒单", () => {
556
557
  setCatalogService([{ provider: "prov1", id: "good-a" }]);
557
558
  const deps = depsWithScripts([SCRIPT]);
558
559
  await actionRun(
559
- { action: "run", name: "/abs/chain.js", args: {}, model: "prov1/good-a" } as never,
560
+ { name: "/abs/chain.js", args: {}, model: "prov1/good-a" } as never,
560
561
  deps as never,
561
562
  undefined,
562
563
  );
@@ -592,7 +593,7 @@ describe("D4-1 按名解析退役(fake registry)", () => {
592
593
  loadAll: vi.fn().mockResolvedValue([chain]),
593
594
  });
594
595
  const err = await actionRun(
595
- { action: "run", name: "chain" } as never,
596
+ { name: "chain" } as never,
596
597
  makeDeps(registry) as never,
597
598
  undefined,
598
599
  ).catch((e: unknown) => e as Error);
@@ -615,7 +616,7 @@ describe("D4-1 按名解析退役(fake registry)", () => {
615
616
  loadAll: vi.fn().mockResolvedValue([byPath]),
616
617
  });
617
618
  await actionRun(
618
- { action: "run", name: "/abs/demo.js" } as never,
619
+ { name: "/abs/demo.js" } as never,
619
620
  makeDeps(registry) as never,
620
621
  undefined,
621
622
  );
@@ -633,7 +634,7 @@ describe("D4-1 按名解析退役(fake registry)", () => {
633
634
  });
634
635
  await expect(
635
636
  actionRun(
636
- { action: "run", name: "ghost" } as never,
637
+ { name: "ghost" } as never,
637
638
  makeDeps(registry) as never,
638
639
  undefined,
639
640
  ),
@@ -646,7 +647,7 @@ describe("D4-1 按名解析退役(fake registry)", () => {
646
647
  loadAll: vi.fn().mockResolvedValue([makeScript("chain", "/builtin/workflows/chain.js")]),
647
648
  });
648
649
  const err = await actionRun(
649
- { action: "run", name: "no-such" } as never,
650
+ { name: "no-such" } as never,
650
651
  makeDeps(registry) as never,
651
652
  undefined,
652
653
  ).catch((e: unknown) => e as Error);
@@ -688,7 +689,7 @@ describe("D4-1 按名解析退役(真 registry:WorkflowScriptRegistryImpl +
688
689
 
689
690
  await expect(
690
691
  actionRun(
691
- { action: "run", name: "chain" } as never,
692
+ { name: "chain" } as never,
692
693
  makeDeps(registry as unknown as Record<string, unknown>) as never,
693
694
  undefined,
694
695
  ),
@@ -704,7 +705,7 @@ describe("D4-1 按名解析退役(真 registry:WorkflowScriptRegistryImpl +
704
705
  npmDirs: [],
705
706
  });
706
707
  const result = await actionRun(
707
- { action: "run", name: join(fixtureDir, "ws", ".pi", "workflows", "chain.js") } as never,
708
+ { name: join(fixtureDir, "ws", ".pi", "workflows", "chain.js") } as never,
708
709
  makeDeps(registry as unknown as Record<string, unknown>) as never,
709
710
  undefined,
710
711
  );
@@ -742,7 +743,7 @@ describe("run 启动文案全文锚定(LLM 可见文本锁)", () => {
742
743
 
743
744
  it("含 slug → 'name · slug (runId)' + 防轮询段(逐字 toBe)", async () => {
744
745
  const result = await actionRun(
745
- { action: "run", name: "/abs/demo.js", slug: "tri-review" } as never,
746
+ { name: "/abs/demo.js", slug: "tri-review" } as never,
746
747
  makeDeps(demoRegistry()) as never,
747
748
  undefined,
748
749
  );
@@ -754,7 +755,7 @@ describe("run 启动文案全文锚定(LLM 可见文本锁)", () => {
754
755
 
755
756
  it("不含 slug → 'name (runId)' 无 · 段(逐字 toBe)", async () => {
756
757
  const result = await actionRun(
757
- { action: "run", name: "/abs/demo.js" } as never,
758
+ { name: "/abs/demo.js" } as never,
758
759
  makeDeps(demoRegistry()) as never,
759
760
  undefined,
760
761
  );
@@ -780,7 +781,7 @@ describe("run details.stateFile = store.stateFilePath(runId)(行为级等值
780
781
  store: storeRef,
781
782
  };
782
783
  const result = await actionRun(
783
- { action: "run", name: "/abs/demo.js" } as never,
784
+ { name: "/abs/demo.js" } as never,
784
785
  deps as never,
785
786
  undefined,
786
787
  );
@@ -831,7 +832,7 @@ describe("abort 转移文案全文锚定(LLM 可见文本锁)", () => {
831
832
  status: "running",
832
833
  startedAt: "2026-01-01T00:00:00.000Z",
833
834
  });
834
- const tool = captureRegisteredTool<AbortToolView>(new Map([[run.runId, run]]));
835
+ const tool = captureRegisteredTool<AbortToolView>(new Map([[run.runId, run]]), "workflow_control");
835
836
 
836
837
  const r = await tool.execute("id", { action: "abort", runId: run.runId }, undefined, undefined, {});
837
838
  expect(r.content[0]?.text).toBe("Workflow 'demo-wf' (wf-1719500000000-a1b2c3): running → done (aborted)");
@@ -849,7 +850,7 @@ describe("abort 转移文案全文锚定(LLM 可见文本锁)", () => {
849
850
  status: "running",
850
851
  startedAt: "2026-01-01T00:00:00.000Z",
851
852
  });
852
- const tool = captureRegisteredTool<AbortToolView>(new Map([[run.runId, run]]));
853
+ const tool = captureRegisteredTool<AbortToolView>(new Map([[run.runId, run]]), "workflow_control");
853
854
 
854
855
  const r = await tool.execute("id", { action: "abort", runId: run.runId }, undefined, undefined, {});
855
856
  expect(r.content[0]?.text).toBe("Workflow 'cleanup-wf' (wf-1719600000000-z9y8x7): running → running");
@@ -897,7 +898,7 @@ describe("TC3i: 宿主 TOOL_TOP_LEVEL reservedKeys 接线锚(M-3 回归)", (
897
898
  // 合法调用不误报
898
899
  expect(
899
900
  findFlattenedArgKeys(
900
- { action: "run", name: "mywf", args: { name: "x" } },
901
+ { name: "mywf", args: { name: "x" } },
901
902
  synthetic,
902
903
  OPT,
903
904
  ),
@@ -907,6 +908,23 @@ describe("TC3i: 宿主 TOOL_TOP_LEVEL reservedKeys 接线锚(M-3 回归)", (
907
908
  });
908
909
  });
909
910
 
911
+ // ══════════════════════════════════════════════════════════════
912
+ // WorkflowRunParams.name 必填性源码锚(tool-declaration-slimming D3 run 侧锁)。
913
+ //
914
+ // SW 包内 vitest 把 typebox alias 到 mock(丢 required),schema 对象断言在本包
915
+ // 环境无防线价值——name 必填性走源码负锚(与 tool-subagents.test.ts tasks 负锚
916
+ // 同款形态):name 未经 Type.Optional 包裹,误改 Optional 即红。
917
+ // actionRun 的 `if (!name) throw` 是运行时兜底,不替代声明面(拍平老路回归时
918
+ // 声明面先劣化,本锚在文本层先亮)。
919
+ // ══════════════════════════════════════════════════════════════
920
+
921
+ describe("WorkflowRunParams.name 必填性源码锚(D3 必填性真实化 run 侧锁)", () => {
922
+ it("name 未经 Type.Optional 包裹(schema 声明面必填性防回归)", () => {
923
+ expect(TOOL_WORKFLOW_SRC).toContain("name: Type.String(");
924
+ expect(TOOL_WORKFLOW_SRC).not.toContain("name: Type.Optional(");
925
+ });
926
+ });
927
+
910
928
  // ══════════════════════════════════════════════════════════════
911
929
  // renderCall 标题行(displayWorkflowName 短名 + runId/slug 不受影响)
912
930
  // ══════════════════════════════════════════════════════════════
@@ -932,40 +950,43 @@ function textOf(component: unknown): string {
932
950
  return "";
933
951
  }
934
952
 
935
- describe("renderCall 标题行:displayWorkflowName 短名 + runId/slug 不受影响", () => {
936
- const tool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false });
953
+ describe("renderCall 标题行:displayWorkflowName 短名 + runId/slug 不受影响(D3 拆分后)", () => {
954
+ const runTool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false }, "workflow_run");
955
+ const controlTool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false }, "workflow_control");
937
956
  const theme = makeRenderTheme();
938
-
939
- it("绝对路径 name → basename 短名(去 .js、去目录)", () => {
940
- const out = textOf(
941
- (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
942
- .renderCall({ action: "run", name: "/home/u/wf/batch-report.js" }, theme),
943
- );
944
- expect(out).toMatch(/batch-report<\/accent>/); // name 落在 accent 段收口前(前缀空格也在段内)
957
+ type RenderCallOf = (args: Record<string, unknown>, theme: unknown) => unknown;
958
+ const renderCallOf = (tool: CapturedTool): RenderCallOf =>
959
+ (tool as unknown as { renderCall: RenderCallOf }).renderCall;
960
+
961
+ it("workflow_run:绝对路径 name → basename 短名(去 .js、去目录)", () => {
962
+ const out = textOf(renderCallOf(runTool)({ name: "/home/u/wf/batch-report.js" }, theme));
963
+ expect(out).toContain("workflow_run");
964
+ expect(out).toContain("batch-report"); // name 落在 accent 段(前缀空格也在段内)
945
965
  expect(out).not.toContain("/home/u/wf/");
946
966
  expect(out).not.toContain(".js");
947
967
  });
948
968
 
949
- it("runId / slug 后缀不受短名影响(runId 截断预览 + slug 全文)", () => {
969
+ it("workflow_run:slug 后缀不受短名影响(slug 全文;runId 属 control 不在此渲染)", () => {
950
970
  const out = textOf(
951
- (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
952
- .renderCall(
953
- { action: "run", name: "/home/u/wf/batch-report.js", slug: "nightly", runId: "wf-run-0123456789" },
954
- theme,
955
- ),
971
+ renderCallOf(runTool)({ name: "/home/u/wf/batch-report.js", slug: "nightly" }, theme),
956
972
  );
957
973
  // 短名 + slug 全文在位
958
974
  expect(out).toContain("batch-report");
959
975
  expect(out).toContain("nightly");
960
- // runId 只出截断预览(ID_PREVIEW_LENGTH = 8 → "wf-run-0"),不泄漏全文
976
+ });
977
+
978
+ it("workflow_control:action + runId 截断预览(ID_PREVIEW_LENGTH = 8)", () => {
979
+ const out = textOf(
980
+ renderCallOf(controlTool)({ action: "abort", runId: "wf-run-0123456789" }, theme),
981
+ );
982
+ expect(out).toContain("workflow_control");
983
+ expect(out).toContain("abort");
961
984
  expect(out).toContain("wf-run-0");
962
985
  expect(out).not.toContain("wf-run-0123456789");
963
986
  });
964
987
 
965
- it("缺 name(status 等无参 action)不崩、不出短名", () => {
966
- expect(() =>
967
- (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
968
- .renderCall({ action: "status" }, theme),
969
- ).not.toThrow();
988
+ it("缺 name(run)/缺 action(control)不崩", () => {
989
+ expect(() => renderCallOf(runTool)({}, theme)).not.toThrow();
990
+ expect(() => renderCallOf(controlTool)({}, theme)).not.toThrow();
970
991
  });
971
992
  });