@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 +6 -5
- package/package.json +11 -7
- package/relay/relay.mjs +45 -2
- package/skills/workflow-script-format/SKILL.md +2 -56
- package/src/host/__tests__/inflight-reporter.test.ts +26 -1
- package/src/host/__tests__/pi-host.test.ts +418 -4
- package/src/host/inflight-reporter.ts +13 -10
- package/src/host/pi-host.ts +205 -17
- package/src/index.ts +21 -72
- package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
- package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
- package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
- package/src/injectors/resource-list-injector.ts +103 -32
- package/src/injectors/subagent-list-injector.ts +4 -28
- package/src/injectors/workflow-list-injector.ts +8 -4
- package/src/interface/__tests__/capture-tool.ts +56 -0
- package/src/interface/__tests__/commands-resume.test.ts +103 -0
- package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
- package/src/interface/__tests__/tool-render.test.ts +105 -2
- package/src/interface/__tests__/tool-subagents.test.ts +24 -26
- package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
- package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
- package/src/interface/__tests__/tool-workflow.test.ts +884 -0
- package/src/interface/bg-notify-render.ts +18 -9
- package/src/interface/command-actions.ts +22 -17
- package/src/interface/commands.ts +58 -12
- package/src/interface/format.ts +50 -16
- package/src/interface/id-preview.ts +4 -4
- package/src/interface/subagent-actions.ts +18 -50
- package/src/interface/subagent-tool.ts +26 -18
- package/src/interface/subagents.ts +5 -11
- package/src/interface/tool-render.ts +14 -30
- package/src/interface/tool-shared.ts +55 -12
- package/src/interface/tool-subagents.ts +21 -11
- package/src/interface/tool-workflow-script.ts +52 -72
- package/src/interface/tool-workflow.ts +345 -139
- package/src/interface/views/WorkflowsView.ts +10 -7
- package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
- package/src/interface/views/detail-content.ts +3 -5
- package/src/jsonl-run-store.ts +656 -544
- package/src/model-events.ts +35 -0
- package/src/session-lifecycle.ts +376 -85
- package/src/subagents-events.ts +56 -0
- package/src/workflow-events.ts +285 -200
- package/src/workflow-notify.ts +456 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
- package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
- package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
- package/src/interface/__tests__/detectors.test.ts +0 -161
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
- package/src/interface/helpers.ts +0 -272
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
- package/src/interface/views/__tests__/record-progress.test.ts +0 -235
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool 提示词 / schema 契约合一(2026-09-27 测试审计组 6:三源并一文件)。
|
|
3
|
+
*
|
|
4
|
+
* 断言对象分两类:
|
|
5
|
+
* 1. 源码文本断言(读 .ts 文本非 import——避免 pi-ai/typebox/pi-tui 等值导入的
|
|
6
|
+
* 重 mock 链):
|
|
7
|
+
* - subagent tool description 行为约束器契约(原 subagent-tool-prompt.test.ts)。
|
|
8
|
+
* agent(LLM)决策时唯一能看到的 tool 元信息就是 description;本组断言锁
|
|
9
|
+
* 调用信号(何时委派)、能力边界(cannot)、反模式密度、注入防御等约束措辞,
|
|
10
|
+
* 防后续重构把约束删掉或弱化。
|
|
11
|
+
* - workflow tool promptGuidelines 契约(原 workflow-tool-prompt.test.ts)。
|
|
12
|
+
* 若 promptGuidelines 不提及内置 workflow 与交叉引用,LLM 无法知道有现成
|
|
13
|
+
* 编排工具可用,会倾向自己 generate 脚本或瞎猜 name。
|
|
14
|
+
* - runtime handler 错误文案 Correct 正例:subagent 侧锚 core 源码(D6② 下沉
|
|
15
|
+
* 后文案权威源在 core subagent-actions-core);workflow 侧原 grep 条已删,
|
|
16
|
+
* 行为版在 tool-workflow.test.ts 平铺检测 throw 条。
|
|
17
|
+
* 2. schema 数据断言(JSON 往返剥离 typebox 类型包装,只看数据形态):
|
|
18
|
+
* collect / conversation 退役契约(原 subagent-schema-collect.test.ts)。
|
|
19
|
+
* required 断言条已删——本包 vitest 把 typebox alias 到 mocks/typebox.ts
|
|
20
|
+
* (丢 options),required 恒缺省致断言提前 return 零执行;真实 typebox 下的
|
|
21
|
+
* required/enum/pattern 锁由 structured-output cross-package-contract.test.ts
|
|
22
|
+
* 承担。
|
|
23
|
+
*
|
|
24
|
+
* 框架:vitest(禁 node:test)。
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { createRequire } from "node:module";
|
|
30
|
+
|
|
31
|
+
import { describe, expect, it } from "vitest";
|
|
32
|
+
|
|
33
|
+
import { SubagentParams } from "../subagent-tool-schema.ts";
|
|
34
|
+
|
|
35
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
|
|
37
|
+
// ── 源码文本源(读源码而非 import,避免 mock 链)──
|
|
38
|
+
|
|
39
|
+
const SUBAGENT_TOOL_SRC = readFileSync(join(__dirname, "../subagent-tool.ts"), "utf-8");
|
|
40
|
+
const TOOL_WORKFLOW_SRC = readFileSync(join(__dirname, "../tool-workflow.ts"), "utf-8");
|
|
41
|
+
const TOOL_WORKFLOW_SCRIPT_SRC = readFileSync(
|
|
42
|
+
join(__dirname, "../tool-workflow-script.ts"),
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/** 提取 description: `...` 模板字符串的原始内容。 */
|
|
47
|
+
function extractDescription(src: string): string {
|
|
48
|
+
const m = src.match(/description:\s*`([\s\S]*?)`,/);
|
|
49
|
+
if (!m) throw new Error("description template literal not found");
|
|
50
|
+
return m[1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DESCRIPTION = extractDescription(SUBAGENT_TOOL_SRC);
|
|
54
|
+
|
|
55
|
+
/** 截取 promptGuidelines 数组文本——防 KNOWN_ARG_KEYS 等注释/代码中的裸词污染断言。
|
|
56
|
+
* 数到数组闭合(跳过字符串字面量内的括号,exec-review F9:'],' 序列静默截断)。 */
|
|
57
|
+
function promptGuidelinesText(src: string): string {
|
|
58
|
+
const start = src.indexOf("promptGuidelines: [");
|
|
59
|
+
let depth = 0;
|
|
60
|
+
let inStr = false;
|
|
61
|
+
for (let i = start; i < src.length; i++) {
|
|
62
|
+
const ch = src[i];
|
|
63
|
+
if (inStr) {
|
|
64
|
+
if (ch === "\\") { i++; continue; }
|
|
65
|
+
if (ch === '"') inStr = false;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === '"') inStr = true;
|
|
69
|
+
else if (ch === "[") depth++;
|
|
70
|
+
else if (ch === "]") {
|
|
71
|
+
depth--;
|
|
72
|
+
if (depth === 0) return src.slice(start, i + 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return src.slice(start);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── schema 数据源(原 subagent-schema-collect.test.ts)──
|
|
79
|
+
|
|
80
|
+
/** 运行时守卫(不用裸类型断言:extensions taste/no-unsafe-cast 规范)。 */
|
|
81
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
82
|
+
return typeof v === "object" && v !== null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** schema 的可检视图:经 JSON 往返剥离 typebox 类型包装,只看数据形态。 */
|
|
86
|
+
function schemaData(): Record<string, unknown> {
|
|
87
|
+
const raw: unknown = JSON.parse(JSON.stringify(SubagentParams));
|
|
88
|
+
if (!isRecord(raw)) throw new Error("SubagentParams is not an object schema");
|
|
89
|
+
return raw;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function properties(): Record<string, unknown> {
|
|
93
|
+
const props = schemaData().properties;
|
|
94
|
+
if (!isRecord(props)) throw new Error("SubagentParams has no properties");
|
|
95
|
+
return props;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ══════════════════════════════════════════════════════════════
|
|
99
|
+
// subagent tool description(原 subagent-tool-prompt.test.ts)
|
|
100
|
+
// ══════════════════════════════════════════════════════════════
|
|
101
|
+
|
|
102
|
+
describe("subagent tool description — 行为约束器(非功能说明书)", () => {
|
|
103
|
+
it("词数 ≤ 850(高风险 description 密度上限)", () => {
|
|
104
|
+
// 高风险 tool 的 description 应聚焦约束而非功能铺陈;过长会稀释信号。
|
|
105
|
+
// 上限演进:400 → 550(补 start/list/cancel 三 action 完整 JSON 正例)→
|
|
106
|
+
// 650(M2-B3 新增 message/close 两 action + conversation 对话模式 + 例子)→
|
|
107
|
+
// 800(递归指导:树形判据 + 自包含 task + 独立验收 + fork 成本警告)→
|
|
108
|
+
// 850(C2 补 sync 批独立判据禁令句——adversarial-review-fixes §3.4:原 description
|
|
109
|
+
// 未定义 independent 且未交叉引用依赖链禁令,依赖任务入批缺判据拦截)。
|
|
110
|
+
// 逼近上限时优先精简正文而非再放宽断言。
|
|
111
|
+
// 每次 action 扩展必然增加必需描述;正例对弱模型首次用对参数的价值 > 节省 description 预算。
|
|
112
|
+
const words = DESCRIPTION.trim().split(/\s+/).filter(Boolean).length;
|
|
113
|
+
expect(words).toBeLessThanOrEqual(850);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("含 'When to delegate' 调用条件段(何时委派 vs 自己做)", () => {
|
|
117
|
+
// 开篇必须给信号驱动的调用条件,而非纯功能说明。
|
|
118
|
+
expect(DESCRIPTION).toMatch(/When to delegate/i);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("Anti-patterns 段含 ≥ 4 条 bullet", () => {
|
|
122
|
+
// 高风险 tool 要求 ≥ 4 条反模式,密度才足以覆盖主要误用路径。
|
|
123
|
+
const apIdx = DESCRIPTION.indexOf("## Anti-patterns");
|
|
124
|
+
expect(apIdx).toBeGreaterThan(-1);
|
|
125
|
+
const afterAp = DESCRIPTION.slice(apIdx);
|
|
126
|
+
// 截到下一个 ## 段
|
|
127
|
+
const nextSection = afterAp.indexOf("##", "## Anti-patterns".length);
|
|
128
|
+
const apSection =
|
|
129
|
+
nextSection > -1 ? afterAp.slice(0, nextSection) : afterAp;
|
|
130
|
+
const bullets = apSection.match(/^- .+/gm) || [];
|
|
131
|
+
expect(bullets.length).toBeGreaterThanOrEqual(4);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("含能力边界段 'You cannot'", () => {
|
|
135
|
+
// 必须显式声明 tool 做不到的事,阻止 LLM 错误假设。
|
|
136
|
+
expect(DESCRIPTION).toMatch(/You cannot/);
|
|
137
|
+
expect(DESCRIPTION.toLowerCase()).toContain("cannot");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("含注入防御:声明 completion message 为不可信数据", () => {
|
|
141
|
+
// completion message 是 auto-injected(F14 注入面),必须告诉 LLM
|
|
142
|
+
// 把它当作不可信数据,校验其中的指令后再执行。
|
|
143
|
+
const lower = DESCRIPTION.toLowerCase();
|
|
144
|
+
expect(
|
|
145
|
+
lower.includes("untrusted") || lower.includes("verify"),
|
|
146
|
+
).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("保留 nested spawning 段(允许 sub-subagent,仅深度限制)", () => {
|
|
150
|
+
// 这段防止 LLM 错误拒绝合法的 nested delegation。
|
|
151
|
+
expect(DESCRIPTION).toMatch(/Nested spawning/);
|
|
152
|
+
expect(DESCRIPTION).toMatch(/Depth: N\/10/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("递归指导含树形判据 + 自包含 task + 独立验收 + 深度定位", () => {
|
|
156
|
+
// 递归不是「能套就套」,必须有明确使用判据,否则 LLM 会滥用深递归。
|
|
157
|
+
const lower = DESCRIPTION.toLowerCase();
|
|
158
|
+
expect(lower).toContain("tree-shaped"); // 树形判据
|
|
159
|
+
expect(lower).toContain("self-contained"); // task 自包含
|
|
160
|
+
expect(lower).toContain("acceptance criteria"); // 独立验收
|
|
161
|
+
expect(lower).toContain("safety rail"); // 深度定位:10 是护栏非预算
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("保留 executionMode sequential 的 CRITICAL 说明", () => {
|
|
165
|
+
// sequential 是关键执行语义,删了会导致 LLM 误以为并行可用。
|
|
166
|
+
expect(DESCRIPTION).toMatch(/CRITICAL/i);
|
|
167
|
+
expect(DESCRIPTION).toMatch(/sequential/);
|
|
168
|
+
expect(DESCRIPTION).toMatch(/SAME message/i);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("Examples 段含平铺 JSON 正例(task/slug 在顶层,无 startParam envelope)", () => {
|
|
172
|
+
// 弱模型信任 schema 结构信号 > 文本信号,原本嵌套 startParam 容器经常被省略。
|
|
173
|
+
// 现已拍平:task/slug 等 13 字段直接放在顶层。description 必须有完整平铺 JSON 正例,
|
|
174
|
+
// 让模型能直接照抄。强约束:startParam envelope 必须从 description 中彻底消失。
|
|
175
|
+
expect(DESCRIPTION).toContain('"action":"start","task"');
|
|
176
|
+
expect(DESCRIPTION).not.toContain('"startParam"');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("cancel 示例 subagentId 用 sa- 连字符前缀(与 subagent-service.ts 实际生成格式一致)", () => {
|
|
180
|
+
// subagent-service.ts:600 生成 `sa-${crypto.randomUUID()}`(连字符)。
|
|
181
|
+
// description 示例必须与实际生成格式一致——弱模型会照抄示例,前缀错(如 sa_ 下划线)
|
|
182
|
+
// 会导致 subagentId 永远匹配不到真实 record。
|
|
183
|
+
expect(DESCRIPTION).toContain('"subagentId":"sa-');
|
|
184
|
+
expect(DESCRIPTION).not.toContain('"sa_');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("Examples 示例 agent 值必须是绝对路径 .md 形态(显式 ref 硬守卫拒绝裸名)", () => {
|
|
188
|
+
// 显式 agent ref 走硬守卫:getRequiredAgentConfig → loadByPath(ref, true),
|
|
189
|
+
// 裸名(normalizeRef 非绝对路径 → null)同步 throw `Invalid agent ref` +
|
|
190
|
+
// <available_subagents> 恢复指引(explicit-agent-ref-guard.test.ts 锁定拒绝路径)。
|
|
191
|
+
// 示例若教裸名(历史漂移:"agent":"coder"),弱模型照抄即必败调用——浪费一轮
|
|
192
|
+
// 并系统性教唆反模式。与 subagentId 格式测试同风格:示例必须与实际契约一致。
|
|
193
|
+
// ① 零裸名:任何 "agent":"<非/>" 形态都禁止
|
|
194
|
+
expect(DESCRIPTION).not.toMatch(/"agent":"(?!\/)[^"]*"/);
|
|
195
|
+
// ② 正例在位且为绝对路径 .md 形态(<available_subagents> 注入的 <location> 形态)
|
|
196
|
+
expect(DESCRIPTION).toMatch(/"agent":"\/[^"]*\.md"/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("agent 字段 description 不写死枚举,指向 <available_subagents>(通用化防漂移)", () => {
|
|
200
|
+
// 原实现把 9 个内置 agent 名写死在 schema field description(防漏),
|
|
201
|
+
// 但枚举与包内 agents/*.md 存在漂移风险(新增/删除 agent 要手改两处)。
|
|
202
|
+
// 现改为指向每 turn 动态注入的 <available_subagents> 列表——防漏职责由注入段承担,
|
|
203
|
+
// 工具描述只保留通用指引(2026-08 通用化重构)。
|
|
204
|
+
expect(SUBAGENT_TOOL_SRC).toContain("available_subagents");
|
|
205
|
+
// 通用化约束:不写死任何具体 agent 名(名字随 agents/*.md 动态变化)。
|
|
206
|
+
// 断言目标收窄到 DESCRIPTION 串(原打 SUBAGENT_TOOL_SRC 全文——文件任意位置
|
|
207
|
+
// 出现 worker 等词即误红,如 import 路径 / 注释 / 标识符)。
|
|
208
|
+
expect(DESCRIPTION).not.toMatch(/orchestrator|code-reviewer|context-builder|worker/);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("Anti-patterns 段明确 list/cancel 仍 nested(防过度泛化 flatten)", () => {
|
|
212
|
+
// PR 只拍平 start,listParam/cancelParam 仍 nested。description 必须明确这一不对称性,
|
|
213
|
+
// 否则弱模型学了「subagent tool 现在平铺」会过度泛化发 {"action":"list","includeFinished":true}。
|
|
214
|
+
const apIdx = DESCRIPTION.indexOf("## Anti-patterns");
|
|
215
|
+
expect(apIdx).toBeGreaterThan(-1);
|
|
216
|
+
const afterAp = DESCRIPTION.slice(apIdx);
|
|
217
|
+
const nextSection = afterAp.indexOf("##", "## Anti-patterns".length);
|
|
218
|
+
const apSection = nextSection > -1 ? afterAp.slice(0, nextSection) : afterAp;
|
|
219
|
+
expect(apSection).toMatch(/list.*nested.*listParam|listParam.*nested/i);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("subagent tool runtime handler — 错误文案含纠正正例", () => {
|
|
224
|
+
// 读源码文本断言 startHandler throw 含 Correct 正例,
|
|
225
|
+
// 让弱模型撞错后第二次能直接照抄正确形态。
|
|
226
|
+
// 拍平后:startParam envelope 删除,平铺 task/slug 是合法形态;
|
|
227
|
+
// 平铺检测 guard(hasFlattenedStartFields)已删除,源码不应再含此表达式。
|
|
228
|
+
it("startHandler throw 含 Correct 纠正正例(平铺形态)", () => {
|
|
229
|
+
// [D6②] startHandler 内核(含 Correct 文案)已下沉 core subagent-actions-core,
|
|
230
|
+
// 断言目标跟随文案权威源。锚点用 ./workflows/* 子入口(exports 映射包根真实目录,
|
|
231
|
+
// 随源码走不依赖 dist 构建——relay-env 锚在 require 条件下落 dist/,CI 不构建
|
|
232
|
+
// workspace 包时 resolve 直接 MODULE_NOT_FOUND)。
|
|
233
|
+
const coreRoot = join(
|
|
234
|
+
dirname(
|
|
235
|
+
createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/workflows/README.md"),
|
|
236
|
+
),
|
|
237
|
+
"..",
|
|
238
|
+
);
|
|
239
|
+
const actionsSrc = readFileSync(
|
|
240
|
+
join(coreRoot, "src/execution/assembly/subagent-actions-core.ts"),
|
|
241
|
+
"utf-8",
|
|
242
|
+
);
|
|
243
|
+
// 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
|
|
244
|
+
// 用 occurrences 计数——至少 3 处。
|
|
245
|
+
const occurrences = (actionsSrc.match(/Correct: \{"action":"start"/g) ?? []).length;
|
|
246
|
+
expect(occurrences).toBeGreaterThanOrEqual(3);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("平铺检测 guard(hasFlattenedStartFields)已从 subagent-tool.ts 删除", () => {
|
|
250
|
+
expect(SUBAGENT_TOOL_SRC).not.toContain('params.action === "start" && !params.startParam');
|
|
251
|
+
expect(SUBAGENT_TOOL_SRC).not.toContain("hasFlattenedStartFields");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ══════════════════════════════════════════════════════════════
|
|
256
|
+
// workflow tool promptGuidelines(原 workflow-tool-prompt.test.ts)
|
|
257
|
+
// ══════════════════════════════════════════════════════════════
|
|
258
|
+
|
|
259
|
+
describe("U1: workflow tool prompt mentions built-in workflows", () => {
|
|
260
|
+
it("TC4a: promptGuidelines 不含具体内置 args 枚举(m4 瘦身——参数知识在 read location)", () => {
|
|
261
|
+
// m4:BUILT-IN 枚举删除,发现职责转移给 <available_workflows> 注入段 + read location。
|
|
262
|
+
// 截取 promptGuidelines 段断言("batch1..batchN" 等在 KNOWN_ARG_KEYS 注释中出现)。
|
|
263
|
+
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
264
|
+
expect(guidelines).not.toContain("chain (sequential");
|
|
265
|
+
expect(guidelines).not.toContain("args: task");
|
|
266
|
+
expect(guidelines).not.toContain("batch1..batchN");
|
|
267
|
+
expect(guidelines).not.toContain("chain/parallel/scatter-gather/map-reduce");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("TC4b: promptGuidelines 引导 read location 获取参数细节(info 已砍,ADR-0003 D5)", () => {
|
|
271
|
+
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
272
|
+
expect(guidelines).toContain("read the <location>");
|
|
273
|
+
expect(guidelines).toContain("script file");
|
|
274
|
+
// info action 已砍,引导语不再含 workflow info
|
|
275
|
+
expect(guidelines).not.toContain("workflow info");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("tool-workflow.ts promptGuidelines 含 workflow-script list 交叉引用", () => {
|
|
279
|
+
// LLM 需要知道"先 list 再 run"的发现路径——两个 tool 之间必须有交叉引用。
|
|
280
|
+
// 文本可能跨字符串拼接行,分别断言两个关键词都存在。
|
|
281
|
+
expect(TOOL_WORKFLOW_SRC).toContain("workflow-script");
|
|
282
|
+
expect(TOOL_WORKFLOW_SRC).toMatch(/action.*list/i);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("tool-workflow.ts promptGuidelines 含 run action 的正例", () => {
|
|
286
|
+
// 给出 run 调用的 JSON 示例,LLM 才知道参数格式(action/name/args 嵌套)。
|
|
287
|
+
expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run","name":"');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("promptGuidelines 含 JSON 调用正例(run/status/lifecycle)", () => {
|
|
291
|
+
// 弱模型信任 schema 结构信号 > 文本信号,容易把 args 子字段平铺到顶层。
|
|
292
|
+
// promptGuidelines 必须有完整 JSON 调用正例,让模型能直接照抄 {"action":"run",...} 嵌套结构。
|
|
293
|
+
expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run"');
|
|
294
|
+
expect(TOOL_WORKFLOW_SRC).toContain("Call shapes (JSON)");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("promptGuidelines 含参数结构反例(args 平铺到顶层)", () => {
|
|
298
|
+
// 显式说明 args 子字段不能平铺到顶层,必须嵌在 args 里。
|
|
299
|
+
expect(TOOL_WORKFLOW_SRC).toContain("args");
|
|
300
|
+
expect(TOOL_WORKFLOW_SRC).toContain("Anti-patterns");
|
|
301
|
+
expect(TOOL_WORKFLOW_SRC).toContain("top level");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("KNOWN_ARG_KEYS 退役守卫(参数知识源 = schema 动态集 argKeysFromMeta,静态枚举不得复活)", () => {
|
|
305
|
+
// 原「runtime handler 错误文案含 Correct 正例 + 平铺检测」grep 条已删:
|
|
306
|
+
// 行为版(Correct 文案 / findFlattenedArgKeys / argKeysFromMeta 真实触发)
|
|
307
|
+
// 在 tool-workflow.test.ts 平铺检测 throw 条与 TC3i 接线条锁定。
|
|
308
|
+
// 退役负向守卫保留:KNOWN_ARG_KEYS 曾是静态已知键枚举,m6 后数据源为动态
|
|
309
|
+
// argKeysFromMeta(schema 即 SSOT)——静态枚举不得复活。
|
|
310
|
+
expect(TOOL_WORKFLOW_SRC).not.toMatch(/const\s+KNOWN_ARG_KEYS/);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow run 交叉引用", () => {
|
|
314
|
+
// 反向交叉引用:list 的指引里要提到用 workflow tool 的 run action 启动脚本。
|
|
315
|
+
expect(TOOL_WORKFLOW_SCRIPT_SRC).toMatch(/workflow.*tool.*run|run.*workflow.*tool/i);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("tool-workflow.ts promptGuidelines 强化 anti-generate(直接 run,不要 generate)", () => {
|
|
319
|
+
// session 证据:弱模型看到 workflow list 后倾向 workflow-script generate 而非直接 run。
|
|
320
|
+
// 提示词必须显式禁止对内置编排使用 generate。
|
|
321
|
+
expect(TOOL_WORKFLOW_SRC).toContain("NEVER use workflow-script action:generate");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("tool-workflow-script.ts promptGuidelines 强化 anti-generate(CRITICAL ANTI-PATTERN)", () => {
|
|
325
|
+
expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("CRITICAL ANTI-PATTERN");
|
|
326
|
+
expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("NEVER generate");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("promptGuidelines + parameter description 标注 budget 默认不限制(B/C:除非用户要求否则别设)", () => {
|
|
330
|
+
// budget 是 run 级参数,运行时 maxTokens===undefined → 不限制(budget.ts isExceeded 守卫)。
|
|
331
|
+
// 但 call shape 示例展示了 tokens/time 字段,LLM 会误以为每次都该填。
|
|
332
|
+
// promptGuidelines 必须明确 "Do NOT set ... unless user explicitly requests",
|
|
333
|
+
// parameter description 同步标 "omit = unlimited",双重约束压过示例的反引导。
|
|
334
|
+
const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
|
|
335
|
+
expect(guidelines).toContain("Do NOT set tokens/time unless the user explicitly requests");
|
|
336
|
+
expect(TOOL_WORKFLOW_SRC).toContain("omit = unlimited (default)");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ══════════════════════════════════════════════════════════════
|
|
341
|
+
// subagent start schema 退役契约(原 subagent-schema-collect.test.ts)
|
|
342
|
+
// ══════════════════════════════════════════════════════════════
|
|
343
|
+
|
|
344
|
+
describe("subagent start schema: collect param retired (批量入口 = subagents tool)", () => {
|
|
345
|
+
it("collect 字段已删(schema 层不可再自报:批量编排唯一入口是 `subagents` tool)", () => {
|
|
346
|
+
expect(Object.keys(properties())).not.toContain("collect");
|
|
347
|
+
// 数据面全量断言(描述文本里的 collect 措辞随之删除):序列化后零残留。
|
|
348
|
+
expect(JSON.stringify(schemaData())).not.toContain("collect");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("sits flattened at top level beside task/slug/engine (拍平契约不变)", () => {
|
|
352
|
+
const props = Object.keys(properties());
|
|
353
|
+
for (const sibling of ["task", "slug", "engine"]) {
|
|
354
|
+
expect(props).toContain(sibling);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("conversation param is deleted (modeless 波5:模式消亡,schema 层不可再自报)", () => {
|
|
359
|
+
expect(Object.keys(properties())).not.toContain("conversation");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -15,7 +15,7 @@ import { describe, expect, it } from "vitest";
|
|
|
15
15
|
|
|
16
16
|
import type { Component } from "@earendil-works/pi-tui";
|
|
17
17
|
|
|
18
|
-
import { type RenderContext, renderSubagentCall } from "../tool-render.ts";
|
|
18
|
+
import { type RenderContext, renderSubagentCall, renderSubagentResult } from "../tool-render.ts";
|
|
19
19
|
|
|
20
20
|
// ── 最小 ThemeLike stub ──
|
|
21
21
|
// renderSubagentCall 只用 theme.fg/bold/dim(都是 (token, text) => string)。
|
|
@@ -46,7 +46,6 @@ function renderText(component: Component): string {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const CTX: RenderContext = {
|
|
49
|
-
state: {} as Record<string, never>,
|
|
50
49
|
invalidate: () => {},
|
|
51
50
|
};
|
|
52
51
|
|
|
@@ -122,3 +121,107 @@ describe("renderSubagentCall — 拍平形态提取(regression for wave 3 flat
|
|
|
122
121
|
expect(() => renderSubagentCall(undefined, makeTheme() as never, CTX)).not.toThrow();
|
|
123
122
|
});
|
|
124
123
|
});
|
|
124
|
+
|
|
125
|
+
// ============================================================
|
|
126
|
+
// renderSubagentResult — expanded / compact 分支等价(字节级回归锁)
|
|
127
|
+
// ============================================================
|
|
128
|
+
//
|
|
129
|
+
// buildExpandedLines 收敛为「compact + list/fork-from 特例」后的守卫:cancel /
|
|
130
|
+
// start(bg) 分支的 expanded 输出必须与 compact 逐字节相同(同一 render 宽度下
|
|
131
|
+
// 的完整字符串数组相等);list / fork-from 的既有差异(session 行追加 / 源文件
|
|
132
|
+
// 完整路径 vs basename)锁住不被收敛误伤。
|
|
133
|
+
|
|
134
|
+
/** renderSubagentResult 的 result 入参形态(经函数签名推导,不依赖 mock 类型桩)。 */
|
|
135
|
+
type RenderResultInput = Parameters<typeof renderSubagentResult>[0];
|
|
136
|
+
|
|
137
|
+
/** 字节级比较用的渲染宽度(大于全部用例行宽,规避 truncLine 截断差异)。 */
|
|
138
|
+
const BYTE_LOCK_WIDTH = 160;
|
|
139
|
+
|
|
140
|
+
function renderBoth(details: unknown): { compact: string[]; expanded: string[] } {
|
|
141
|
+
const result = { content: [{ type: "text", text: "{}" }], details } as RenderResultInput;
|
|
142
|
+
const theme = makeTheme() as never;
|
|
143
|
+
const compact = renderSubagentResult(
|
|
144
|
+
result,
|
|
145
|
+
{ expanded: false, isPartial: false },
|
|
146
|
+
theme,
|
|
147
|
+
CTX,
|
|
148
|
+
).render(BYTE_LOCK_WIDTH);
|
|
149
|
+
const expanded = renderSubagentResult(
|
|
150
|
+
result,
|
|
151
|
+
{ expanded: true, isPartial: false },
|
|
152
|
+
theme,
|
|
153
|
+
CTX,
|
|
154
|
+
).render(BYTE_LOCK_WIDTH);
|
|
155
|
+
return { compact, expanded };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
describe("renderSubagentResult — expanded 与 compact 的分支关系(字节级)", () => {
|
|
159
|
+
it("cancel:expanded 输出与 compact 逐字节相同", () => {
|
|
160
|
+
const details = {
|
|
161
|
+
action: "cancel",
|
|
162
|
+
subagentId: "sa-cancel1",
|
|
163
|
+
sessionFile: null,
|
|
164
|
+
cancelResponse: { cancelled: true },
|
|
165
|
+
};
|
|
166
|
+
const { compact, expanded } = renderBoth(details);
|
|
167
|
+
expect(expanded).toEqual(compact);
|
|
168
|
+
expect(expanded.join("\n")).toContain("cancelled ");
|
|
169
|
+
expect(expanded.join("\n")).toContain("sa-cancel1");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("start(bg):expanded 输出与 compact 逐字节相同", () => {
|
|
173
|
+
const details = {
|
|
174
|
+
action: "start",
|
|
175
|
+
subagentId: "sa-start1",
|
|
176
|
+
sessionFile: null,
|
|
177
|
+
slug: "fix-login",
|
|
178
|
+
model: undefined,
|
|
179
|
+
bgResponse: { status: "running", mode: "background", message: "detached" },
|
|
180
|
+
};
|
|
181
|
+
const { compact, expanded } = renderBoth(details);
|
|
182
|
+
expect(expanded).toEqual(compact);
|
|
183
|
+
expect(expanded.join("\n")).toContain("background: ");
|
|
184
|
+
expect(expanded.join("\n")).toContain("sa-start1");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("fork-from:expanded 用完整源路径,compact 用 basename 短标签(差异锁定)", () => {
|
|
188
|
+
const details = {
|
|
189
|
+
action: "fork-from",
|
|
190
|
+
subagentId: "sa-new1",
|
|
191
|
+
sessionFile: null,
|
|
192
|
+
forkFromResponse: {
|
|
193
|
+
newSubagentId: "sa-new1",
|
|
194
|
+
sourceSessionFile: "/tmp/sessions/old-abc.jsonl",
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
const { compact, expanded } = renderBoth(details);
|
|
198
|
+
expect(compact.join("\n")).not.toContain("/tmp/sessions/");
|
|
199
|
+
expect(compact.join("\n")).toContain("old-abc.jsonl");
|
|
200
|
+
expect(expanded.join("\n")).toContain("/tmp/sessions/old-abc.jsonl");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("list:expanded 在 compact 之上追加 session 行(差异锁定)", () => {
|
|
204
|
+
const details = {
|
|
205
|
+
action: "list",
|
|
206
|
+
subagentId: null,
|
|
207
|
+
sessionFile: null,
|
|
208
|
+
listResponse: {
|
|
209
|
+
running: 1,
|
|
210
|
+
items: [{
|
|
211
|
+
subagentId: "sa-1",
|
|
212
|
+
agent: "worker",
|
|
213
|
+
slug: "scan",
|
|
214
|
+
status: "running",
|
|
215
|
+
duration: 12,
|
|
216
|
+
sessionFile: "/tmp/sessions/sa-1.jsonl",
|
|
217
|
+
}],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
const { compact, expanded } = renderBoth(details);
|
|
221
|
+
expect(compact.join("\n")).not.toContain("session: ");
|
|
222
|
+
expect(expanded.join("\n")).toContain("/tmp/sessions/sa-1.jsonl");
|
|
223
|
+
expect(compact.join("\n")).not.toContain("/tmp/sessions/sa-1.jsonl");
|
|
224
|
+
// compact 的行是 expanded 的前缀(追加不修改既有行)
|
|
225
|
+
expect(expanded.slice(0, compact.length)).toEqual(compact);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
runSubagentsBatch,
|
|
35
35
|
} from "../tool-subagents.ts";
|
|
36
36
|
import { REENTRY_BUSY_MESSAGE, type ReentryGuardRef } from "../reentry-guard.ts";
|
|
37
|
+
import { captureTool } from "./capture-tool.ts";
|
|
37
38
|
|
|
38
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
40
|
const TOOL_SRC = readFileSync(join(__dirname, "../tool-subagents.ts"), "utf-8");
|
|
@@ -45,7 +46,7 @@ interface CapturedProperty {
|
|
|
45
46
|
optional?: boolean;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
interface
|
|
49
|
+
interface SubagentsToolView {
|
|
49
50
|
name: string;
|
|
50
51
|
label: string;
|
|
51
52
|
description: string;
|
|
@@ -68,17 +69,14 @@ interface ThemeStub {
|
|
|
68
69
|
bold(text: string): string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** 注册并取回 tool 定义(fake pi
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
registerSubagentsTool(pi as never, makeDeps(registry) as never, reentryRef);
|
|
80
|
-
expect(captured).toHaveLength(1);
|
|
81
|
-
return captured[0] as CapturedTool;
|
|
72
|
+
/** 注册并取回 tool 定义(fake pi 捕获单点见 capture-tool.ts;此处只留差异面)。 */
|
|
73
|
+
function captureSubagentsTool(
|
|
74
|
+
reentryRef: ReentryGuardRef,
|
|
75
|
+
registry: Record<string, unknown>,
|
|
76
|
+
): SubagentsToolView {
|
|
77
|
+
return captureTool<SubagentsToolView>(
|
|
78
|
+
(pi) => registerSubagentsTool(pi as never, makeDeps(registry) as never, reentryRef),
|
|
79
|
+
);
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
// ── registry / deps stubs ──
|
|
@@ -177,7 +175,7 @@ function lastSpec(): Record<string, unknown> {
|
|
|
177
175
|
// ══════════════════════════════════════════════════════════════
|
|
178
176
|
describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
|
|
179
177
|
it("注册名/label/description 存在,且以 batch 语义自述(one-shot 成员 + 一条通知)", () => {
|
|
180
|
-
const tool =
|
|
178
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
181
179
|
expect(tool.name).toBe("subagents");
|
|
182
180
|
expect(tool.label.length).toBeGreaterThan(0);
|
|
183
181
|
expect(tool.description).toContain("one-shot batch member");
|
|
@@ -188,7 +186,7 @@ describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
|
|
|
188
186
|
});
|
|
189
187
|
|
|
190
188
|
it("参数全平铺:无 action 分发、无 args 嵌套、无嵌套 object 参数", () => {
|
|
191
|
-
const tool =
|
|
189
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
192
190
|
const keys = Object.keys(tool.parameters.properties);
|
|
193
191
|
expect(keys.sort()).toEqual(
|
|
194
192
|
["agents", "aggregate", "model", "slug", "tasks", "thinkingLevel", "time", "tokens"].sort(),
|
|
@@ -201,7 +199,7 @@ describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
|
|
|
201
199
|
});
|
|
202
200
|
|
|
203
201
|
it("tasks 为必填形态(未经 Type.Optional 构造)且类型为数组", () => {
|
|
204
|
-
const tool =
|
|
202
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
205
203
|
expect(tool.parameters.properties.tasks?.type).toBe("array");
|
|
206
204
|
// mock typebox 的 Optional 标记:tasks 不得带该标记(真 typebox 下即 required)
|
|
207
205
|
expect(tool.parameters.properties.tasks?.optional).toBeUndefined();
|
|
@@ -231,7 +229,7 @@ describe("schema 契约:一跳扁平 + tasks 必填(D2)", () => {
|
|
|
231
229
|
});
|
|
232
230
|
|
|
233
231
|
it("promptGuidelines 含分工句/调用正例/预算默认(弱模型结构信号依赖面)", () => {
|
|
234
|
-
const tool =
|
|
232
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
235
233
|
const guidelines = tool.promptGuidelines.join("\n");
|
|
236
234
|
expect(guidelines).toContain("2+ independent tasks in one dispatch");
|
|
237
235
|
expect(guidelines).toContain('{"tasks":[');
|
|
@@ -350,14 +348,14 @@ describe("错误规格(D9)", () => {
|
|
|
350
348
|
it("tasks 缺失 → throw 带 Correct 示例(runWorkflow 未被调用)", async () => {
|
|
351
349
|
const err = await runSubagentsBatch({} as never, makeDeps(makeRegistry()) as never, undefined)
|
|
352
350
|
.catch((e: unknown) => e as Error);
|
|
353
|
-
expect(err.message).toBe('tasks
|
|
351
|
+
expect(err.message).toBe('subagents requires \'tasks\' parameter (non-empty string array). Correct: {"tasks":["...","..."]}');
|
|
354
352
|
expect(vi.mocked(runWorkflow)).not.toHaveBeenCalled();
|
|
355
353
|
});
|
|
356
354
|
|
|
357
355
|
it("tasks 空数组 → 同一 Correct 文案", async () => {
|
|
358
356
|
const err = await runSubagentsBatch({ tasks: [] }, makeDeps(makeRegistry()) as never, undefined)
|
|
359
357
|
.catch((e: unknown) => e as Error);
|
|
360
|
-
expect(err.message).toContain("tasks
|
|
358
|
+
expect(err.message).toContain("subagents requires 'tasks' parameter (non-empty string array)");
|
|
361
359
|
expect(vi.mocked(runWorkflow)).not.toHaveBeenCalled();
|
|
362
360
|
});
|
|
363
361
|
|
|
@@ -414,7 +412,7 @@ describe("错误规格(D9)", () => {
|
|
|
414
412
|
// ══════════════════════════════════════════════════════════════
|
|
415
413
|
describe("reentry guard:与 workflow tool 共用同一实例", () => {
|
|
416
414
|
it("guard 被占用 → throw busy 文案,且不触达 runWorkflow", async () => {
|
|
417
|
-
const tool =
|
|
415
|
+
const tool = captureSubagentsTool({ isProcessing: true }, makeRegistry());
|
|
418
416
|
await expect(
|
|
419
417
|
tool.execute("call-1", { tasks: TWO_TASKS }, undefined, undefined, undefined),
|
|
420
418
|
).rejects.toThrow(REENTRY_BUSY_MESSAGE);
|
|
@@ -423,7 +421,7 @@ describe("reentry guard:与 workflow tool 共用同一实例", () => {
|
|
|
423
421
|
|
|
424
422
|
it("成功路径释放 guard(同一 guard 可再次进入)", async () => {
|
|
425
423
|
const guard: ReentryGuardRef = { isProcessing: false };
|
|
426
|
-
const tool =
|
|
424
|
+
const tool = captureSubagentsTool(guard, makeRegistry());
|
|
427
425
|
await tool.execute("call-1", { tasks: TWO_TASKS }, undefined, undefined, undefined);
|
|
428
426
|
expect(guard.isProcessing).toBe(false);
|
|
429
427
|
await tool.execute("call-2", { tasks: TWO_TASKS }, undefined, undefined, undefined);
|
|
@@ -432,15 +430,15 @@ describe("reentry guard:与 workflow tool 共用同一实例", () => {
|
|
|
432
430
|
|
|
433
431
|
it("失败路径经 finally 释放 guard(错误不粘住批量入口)", async () => {
|
|
434
432
|
const guard: ReentryGuardRef = { isProcessing: false };
|
|
435
|
-
const tool =
|
|
433
|
+
const tool = captureSubagentsTool(guard, makeRegistry());
|
|
436
434
|
const err = await tool.execute("call-1", {}, undefined, undefined, undefined).catch((e: unknown) => e as Error);
|
|
437
|
-
expect(err.message).toContain("tasks
|
|
435
|
+
expect(err.message).toContain("subagents requires 'tasks' parameter");
|
|
438
436
|
expect(guard.isProcessing).toBe(false);
|
|
439
437
|
});
|
|
440
438
|
|
|
441
439
|
it("signal 已 abort → 起步即 throw(不占 guard)", async () => {
|
|
442
440
|
const guard: ReentryGuardRef = { isProcessing: false };
|
|
443
|
-
const tool =
|
|
441
|
+
const tool = captureSubagentsTool(guard, makeRegistry());
|
|
444
442
|
const aborted = AbortSignal.abort();
|
|
445
443
|
await expect(
|
|
446
444
|
tool.execute("call-1", { tasks: TWO_TASKS }, aborted as never, undefined, undefined),
|
|
@@ -465,7 +463,7 @@ describe("集合收录(D1:单收录 WORKFLOW,不进 SUBAGENT)", () => {
|
|
|
465
463
|
// ══════════════════════════════════════════════════════════════
|
|
466
464
|
describe("TUI 渲染(renderCall/renderResult 既有惯例)", () => {
|
|
467
465
|
it("renderCall 单行:subagents + 任务数 + slug", () => {
|
|
468
|
-
const tool =
|
|
466
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
469
467
|
const text = renderedText(tool.renderCall(
|
|
470
468
|
{ tasks: TWO_TASKS, slug: "foo-tri-review" },
|
|
471
469
|
makeTheme(),
|
|
@@ -477,13 +475,13 @@ describe("TUI 渲染(renderCall/renderResult 既有惯例)", () => {
|
|
|
477
475
|
});
|
|
478
476
|
|
|
479
477
|
it("renderCall 无 slug/无 tasks 时不崩(streaming partial args 形态)", () => {
|
|
480
|
-
const tool =
|
|
478
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
481
479
|
expect(() => tool.renderCall({}, makeTheme())).not.toThrow();
|
|
482
480
|
expect(() => tool.renderCall({ tasks: "not-an-array" }, makeTheme())).not.toThrow();
|
|
483
481
|
});
|
|
484
482
|
|
|
485
483
|
it("renderResult 透出返回文案(多行保留)", () => {
|
|
486
|
-
const tool =
|
|
484
|
+
const tool = captureSubagentsTool({ isProcessing: false }, makeRegistry());
|
|
487
485
|
const text = renderedText(tool.renderResult(
|
|
488
486
|
{ content: [{ type: "text", text: "Started batch 'x' (wf-1) as workflow run 'fan-out'" }] },
|
|
489
487
|
undefined,
|