@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.
- package/README.md +19 -5
- package/package.json +8 -8
- 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/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
|
@@ -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
|
|
184
|
-
expect(tool.description).toContain("`
|
|
185
|
-
expect(tool.description).toContain("`
|
|
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("
|
|
237
|
-
expect(guidelines).toContain("
|
|
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(
|
|
308
|
-
expect(text).toContain(
|
|
309
|
-
// status/abort 不复制:本 tool 无 action 参数(schema 已断言),文案只指路
|
|
310
|
-
|
|
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:与
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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
|
-
{
|
|
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: \{"
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
);
|
|
944
|
-
expect(out).
|
|
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("
|
|
969
|
+
it("workflow_run:slug 后缀不受短名影响(slug 全文;runId 属 control 不在此渲染)", () => {
|
|
950
970
|
const out = textOf(
|
|
951
|
-
(
|
|
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
|
-
|
|
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(
|
|
966
|
-
expect(() =>
|
|
967
|
-
|
|
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
|
});
|