@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.
Files changed (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +11 -7
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +376 -85
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
@@ -1,376 +0,0 @@
1
- // subagent-list-injector 单测
2
- //
3
- // 两类覆盖:
4
- // 1. 纯函数(parseAgentFrontmatter / formatAgentList):P3 正向触发引导语 + 名字约束
5
- // + XML 结构 + 转义。discoverAllAgents 依赖文件系统 + resource-discovery,属集成层,
6
- // 此处聚焦可快速回归的格式化契约(TC5 回归保护)。
7
- // 2. session 级缓存行为(TC1-TC4):mock shared/resource-discovery 的 discoverResources
8
- // (vi.hoisted 稳定 spy + vi.mock 工厂闭包)+ getCachedFileContent 返回 fixture,
9
- // mock pi.on 捕获三 handler 手动触发;模块级缓存靠 vi.resetModules + 动态 import 重置。
10
-
11
- import { beforeEach, describe, expect, it, vi } from "vitest";
12
-
13
- import type { DiscoveredResource } from "@zhushanwen/subagent-core/shared/resource-discovery.ts";
14
- // 共享 mock 基建(vi.mock 工厂 / mock pi / mock ctx):helpers/injector-test-mocks.ts
15
- // ——必须先于 barrel import 初始化:barrel 的 re-export 链会触发深路径 mock 工厂,
16
- // 工厂闭包引用本 helpers 的绑定(helpers 文件头「惰性求值」约束)
17
- import { createDiscoveryModuleMock, createLoggerModuleMock, createMockCtx, createMockPi, type CapturedHandlers } from "./helpers/injector-test-mocks.ts";
18
- // C5①:formatAgentList 下沉 core barrel——测试改从 barrel 取实现,guide 用 pi 宿主注入文案
19
- import { formatAgentList } from "@zhushanwen/subagent-core";
20
-
21
- // ── 稳定 spy:vi.hoisted 保证 resetModules 后引用不变(vi.mock 工厂闭包捕获同一 fn,
22
- // 故 fresh import 的注入器拿到的 discoverResources === spies.discoverResources,跨
23
- // resetModules 调用计数连续,mockClear 控制每用例重置) ──
24
- const spies = vi.hoisted(() => ({ discoverResources: vi.fn(), getCachedFileContent: vi.fn() }));
25
-
26
- vi.mock("@zhushanwen/subagent-core/shared/resource-discovery.ts", () => createDiscoveryModuleMock(spies));
27
-
28
- // 工厂必须写成箭头惰性形式:vi.mock 被提升到 import 之前执行,直接传
29
- // createLoggerModuleMock 引用会在提升位置立即求值 import 绑定 → TDZ ReferenceError
30
- vi.mock("@zhushanwen/pi-extension-logger", () => createLoggerModuleMock());
31
-
32
- // ── 纯函数测试:静态 import(模块级缓存状态不影响纯函数;与下方缓存 describe 隔离) ──
33
- import { parseAgentFrontmatter, SUBAGENT_LIST_GUIDE } from "../subagent-list-injector";
34
-
35
- describe("parseAgentFrontmatter", () => {
36
- it("解析双引号包裹的 name + description", () => {
37
- const md = `---
38
- name: worker
39
- description: "编码执行者"
40
- ---
41
- body`;
42
- expect(parseAgentFrontmatter(md)).toEqual({
43
- name: "worker",
44
- description: "编码执行者",
45
- path: "",
46
- });
47
- });
48
-
49
- it("解析单引号包裹的 name + description", () => {
50
- const md = `---
51
- name: 'reviewer'
52
- description: '代码审查'
53
- ---`;
54
- expect(parseAgentFrontmatter(md)).toEqual({
55
- name: "reviewer",
56
- description: "代码审查",
57
- path: "",
58
- });
59
- });
60
-
61
- it("缺 name 或 description 时返回 null", () => {
62
- expect(parseAgentFrontmatter("---\nname: worker\n---")).toBeNull();
63
- expect(parseAgentFrontmatter("---\ndescription: x\n---")).toBeNull();
64
- });
65
-
66
- it("无 frontmatter(不以 --- 开头)返回 null", () => {
67
- expect(parseAgentFrontmatter("just markdown")).toBeNull();
68
- });
69
-
70
- it("frontmatter 未闭合(无结束 ---)返回 null", () => {
71
- expect(parseAgentFrontmatter("---\nname: worker\ndescription: x")).toBeNull();
72
- });
73
- });
74
-
75
- describe("formatAgentList", () => {
76
- it("空列表返回空串(不注入)", () => {
77
- expect(formatAgentList([], { guide: SUBAGENT_LIST_GUIDE })).toBe("");
78
- });
79
-
80
- it("TC1: when + examples 注入(原样渲染 + escapeXml + 缺省兼容)", () => {
81
- const out = formatAgentList([
82
- {
83
- name: "reviewer",
84
- description: "代码审查",
85
- path: "/agents/reviewer.md",
86
- when: "用户要求 review 代码",
87
- examples: [
88
- { match: "帮我 review 这段代码", action: "调用 reviewer 对抗式审查", positive: true },
89
- { match: "帮我 review 设计文档", action: "不调用(文档审查应选 doc-reviewer)", positive: false },
90
- ],
91
- },
92
- { name: "legacy", description: "未迁移 agent", path: "/agents/legacy.md" },
93
- ], { guide: SUBAGENT_LIST_GUIDE });
94
- expect(out).toContain("<when>用户要求 review 代码</when>");
95
- // 正反原样渲染——negative action 含原因文本(评审 M5:渲染器不硬编码)
96
- expect(out).toContain('"帮我 review 这段代码" → 调用 reviewer 对抗式审查');
97
- expect(out).toContain('"帮我 review 设计文档" → 不调用(文档审查应选 doc-reviewer)');
98
- // escapeXml:match 含 < > 被转义
99
- const xmlOut = formatAgentList([
100
- {
101
- name: "x",
102
- description: "d",
103
- path: "/agents/x.md",
104
- examples: [{ match: "处理 <task> 的 diff", action: "调用 x", positive: true }],
105
- },
106
- ], { guide: SUBAGENT_LIST_GUIDE });
107
- expect(xmlOut).toContain("&lt;task&gt;");
108
- // 缺省兼容:无 when/examples 的 agent 不渲染该段
109
- const legacyOut = formatAgentList([{ name: "legacy", description: "未迁移 agent", path: "/agents/legacy.md" }], { guide: SUBAGENT_LIST_GUIDE });
110
- expect(legacyOut).not.toContain("<when>");
111
- expect(legacyOut).not.toContain("<examples>");
112
- });
113
-
114
- it("包含 P3 正向触发引导语(何时该 delegate)", () => {
115
- const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
116
- expect(out).toContain("PRIORITY");
117
- expect(out).toContain("3+ files");
118
- expect(out).toContain("delegate");
119
- expect(out).toContain("FIRST");
120
- });
121
-
122
- it("C5① guide 更新:agent 引用改为 <location> 路径形态,动态 agent 指引改为缺省 general-purpose(systemPrompt 参数已不在 schema)", () => {
123
- const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
124
- expect(out).toContain("ONLY use agents from this list");
125
- expect(out).toContain("pass the <location> path (absolute .md path) as the agent param");
126
- expect(out).toContain("omit agent (a general-purpose agent is used)");
127
- // 旧句已过期(subagent tool schema 无 systemPrompt 参数,见 subagent-tool-schema.ts)
128
- expect(out).not.toContain("systemPrompt");
129
- expect(out).not.toContain("ONLY use agent names from this list");
130
- });
131
-
132
- it("包含 'Do NOT call list to discover' 引导语", () => {
133
- const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
134
- expect(out).toContain("Do NOT call list to discover");
135
- expect(out).toContain("use list only for running state");
136
- });
137
-
138
- it("用 <available_subagents> 标签包裹并列出每个 agent", () => {
139
- const out = formatAgentList([
140
- { name: "worker", description: "does work", path: "/agents/worker.md" },
141
- { name: "reviewer", description: "reviews code", path: "/agents/reviewer.md" },
142
- ], { guide: SUBAGENT_LIST_GUIDE });
143
- expect(out).toContain("<available_subagents>");
144
- expect(out).toContain("</available_subagents>");
145
- expect(out).toContain("<name>worker</name>");
146
- expect(out).toContain("<description>does work</description>");
147
- expect(out).toContain("<name>reviewer</name>");
148
- expect(out).toContain("<description>reviews code</description>");
149
- // S1:每项含 <location> 完整路径(agentRef,模型直接引用)
150
- expect(out).toContain("<location>/agents/worker.md</location>");
151
- expect(out).toContain("<location>/agents/reviewer.md</location>");
152
- });
153
-
154
- it("转义 XML 特殊字符", () => {
155
- const out = formatAgentList([{ name: "a&b<c>", description: "\"q\"", path: "/agents/a&b<c>.md" }], { guide: SUBAGENT_LIST_GUIDE });
156
- expect(out).toContain("<name>a&amp;b&lt;c&gt;</name>");
157
- expect(out).toContain("&quot;q&quot;");
158
- });
159
-
160
- it("CA2 快照等价锚定:骨架 + 条目段逐字节等于 pi 旧本地实现模板(除 location 前缀外零变化)", () => {
161
- // 条目模板逐字复制自 C5① 前的本地 formatAgentList 实现(escapeXml 同函数),
162
- // 锚定 core 下沉未改变渲染字节(红线 8:等价豁免仅 location 前缀 + guide 段)
163
- const agents = [
164
- {
165
- name: "reviewer",
166
- description: "代码审查",
167
- path: "/OLD-PREFIX/agents/reviewer.md",
168
- when: "用户要求 review",
169
- },
170
- { name: "worker", description: "does work", path: "/OLD-PREFIX/agents/worker.md" },
171
- ];
172
- const expectedItems = [
173
- ' <agent><name>reviewer</name><description>代码审查</description><when>用户要求 review</when><location>/OLD-PREFIX/agents/reviewer.md</location></agent>',
174
- ' <agent><name>worker</name><description>does work</description><location>/OLD-PREFIX/agents/worker.md</location></agent>',
175
- ];
176
- const out = formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE });
177
- expect(out).toBe(
178
- `\n\n<available_subagents>\n${SUBAGENT_LIST_GUIDE}\n${expectedItems.join("\n")}\n</available_subagents>`,
179
- );
180
-
181
- // location 前缀替换(资产来源迁移:pi-sw 安装目录 → core 包)后其余字节零变化
182
- const migrated = formatAgentList(
183
- agents.map((a) => ({ ...a, path: a.path.replace("/OLD-PREFIX/", "/NEW-CORE-PKG/") })),
184
- { guide: SUBAGENT_LIST_GUIDE },
185
- );
186
- expect(migrated).toBe(out.replaceAll("/OLD-PREFIX/", "/NEW-CORE-PKG/"));
187
- });
188
- });
189
-
190
- // ──────────────────────────────────────────────────────────────
191
- // session 级缓存行为(TC1-TC4;mock pi/ctx 构造在 helpers/injector-test-mocks.ts)
192
- // ──────────────────────────────────────────────────────────────
193
-
194
- /** fixture:单个 worker agent 的 DiscoveredResource。 */
195
- function agentResource(path: string): DiscoveredResource {
196
- return { path, source: "project-agents", available: true };
197
- }
198
-
199
- /** fixture:agent .md frontmatter 内容。 */
200
- function agentMd(name: string, description: string): string {
201
- return `---\nname: ${name}\ndescription: "${description}"\n---\nbody`;
202
- }
203
-
204
- describe("subagent-list-injector session 级缓存", () => {
205
- let setupSubagentListInjector: typeof import("../subagent-list-injector").setupSubagentListInjector;
206
- let handlers: CapturedHandlers;
207
-
208
- beforeEach(async () => {
209
- // resetModules + 动态 import:拿 fresh 模块实例 → 模块级 agentCache 重置为 null
210
- vi.resetModules();
211
- spies.discoverResources.mockReset();
212
- spies.getCachedFileContent.mockReset();
213
- // 默认空发现(各 TC 按需覆盖)
214
- spies.discoverResources.mockResolvedValue([]);
215
- spies.getCachedFileContent.mockReturnValue(null);
216
- handlers = {};
217
- const mod = await import("../subagent-list-injector");
218
- setupSubagentListInjector = mod.setupSubagentListInjector;
219
- setupSubagentListInjector(createMockPi(handlers));
220
- });
221
-
222
- it("TC1: session_start 发现+缓存后,两次 before_agent_start 命中缓存(discoverResources 只调 1 次)", async () => {
223
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/worker.md")]);
224
- spies.getCachedFileContent.mockReturnValue(agentMd("worker", "编码执行者"));
225
-
226
- // session_start 触发发现+缓存
227
- await handlers.sessionStart!({ type: "session_start", reason: "new" }, createMockCtx());
228
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
229
-
230
- // 两次 before_agent_start:均读缓存,不再触发 discoverResources
231
- const r1 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
232
- const r2 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
233
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
234
-
235
- // 两次注入段都含 <available_subagents> 且内容一致(同一缓存)
236
- expect(r1?.systemPrompt).toContain("<available_subagents>");
237
- expect(r1?.systemPrompt).toContain("<name>worker</name>");
238
- expect(r2?.systemPrompt).toBe(r1?.systemPrompt);
239
- });
240
-
241
- it("TC2: session_shutdown 清缓存后 before_agent_start miss → fallback 重新发现+缓存", async () => {
242
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/worker.md")]);
243
- spies.getCachedFileContent.mockReturnValue(agentMd("worker", "编码"));
244
-
245
- // session_start 发现+缓存(discoverResources count=1)
246
- await handlers.sessionStart!({ type: "session_start", reason: "new" }, createMockCtx());
247
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
248
-
249
- // session_shutdown 清缓存
250
- handlers.sessionShutdown!({ type: "session_shutdown", reason: "quit" }, createMockCtx());
251
-
252
- // before_agent_start 读到空缓存 → fallback 重新发现(count=2)+ 重新缓存
253
- const r1 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
254
- expect(spies.discoverResources).toHaveBeenCalledTimes(2);
255
- expect(r1?.systemPrompt).toContain("<name>worker</name>");
256
-
257
- // 再次 before_agent_start 命中缓存(不再调 discoverResources,count 仍 2)
258
- const r2 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
259
- expect(spies.discoverResources).toHaveBeenCalledTimes(2);
260
- expect(r2?.systemPrompt).toBe(r1?.systemPrompt);
261
- });
262
-
263
- it("TC3: 无 session_start 直接 before_agent_start(miss fallback)→ 发现+缓存", async () => {
264
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/worker.md")]);
265
- spies.getCachedFileContent.mockReturnValue(agentMd("worker", "编码"));
266
-
267
- // 未触发 session_start,直接两次 before_agent_start
268
- const r1 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
269
- const r2 = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
270
-
271
- // 第一次 fallback 发现+缓存(count=1);第二次命中缓存(count 仍 1)
272
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
273
- expect(r1?.systemPrompt).toContain("<name>worker</name>");
274
- expect(r2?.systemPrompt).toBe(r1?.systemPrompt);
275
- });
276
-
277
- it("TC4: session_start(reload) 覆盖缓存(新资源生效)", async () => {
278
- // 第一次 session_start(new):发现 worker
279
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/worker.md")]);
280
- spies.getCachedFileContent.mockReturnValue(agentMd("worker", "编码"));
281
- await handlers.sessionStart!({ type: "session_start", reason: "new" }, createMockCtx());
282
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
283
-
284
- // 资源变化:discoverResources 改返回 reviewer,getCachedFileContent 改返回 reviewer 内容
285
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/reviewer.md")]);
286
- spies.getCachedFileContent.mockReturnValue(agentMd("reviewer", "审查"));
287
-
288
- // session_start(reload) 重新发现覆盖缓存(count=2)
289
- await handlers.sessionStart!({ type: "session_start", reason: "reload" }, createMockCtx());
290
- expect(spies.discoverResources).toHaveBeenCalledTimes(2);
291
-
292
- // before_agent_start 命中新缓存:注入段反映 reviewer(非旧 worker)
293
- const r = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
294
- expect(spies.discoverResources).toHaveBeenCalledTimes(2);
295
- expect(r?.systemPrompt).toContain("<name>reviewer</name>");
296
- expect(r?.systemPrompt).not.toContain("worker");
297
- });
298
-
299
- it("session_start 发现空列表缓存后 before_agent_start 命中(不重扫,不注入)", async () => {
300
- // 空列表是有效缓存态(非 null):命中后不重扫,formatAgentList([]) 返空串不注入
301
- spies.discoverResources.mockResolvedValue([]);
302
- await handlers.sessionStart!({ type: "session_start", reason: "new" }, createMockCtx());
303
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
304
-
305
- const r1 = await handlers.beforeAgentStart!({ systemPrompt: "base" }, createMockCtx());
306
- const r2 = await handlers.beforeAgentStart!({ systemPrompt: "base" }, createMockCtx());
307
- expect(spies.discoverResources).toHaveBeenCalledTimes(1);
308
- // 空注入:handler 返 void(无 systemPrompt 字段)
309
- expect(r1).toBeUndefined();
310
- expect(r2).toBeUndefined();
311
- });
312
-
313
- it("session_start 发现异常不阻断(fail-safe,缓存保持 null,before_agent_start fallback)", async () => {
314
- spies.discoverResources.mockRejectedValueOnce(new Error("disk io"));
315
- // session_start 发现抛错:记日志不阻断,缓存保持 null
316
- await handlers.sessionStart!({ type: "session_start", reason: "new" }, createMockCtx());
317
-
318
- // before_agent_start 读到 null 缓存 → fallback 重新发现(recover)
319
- spies.discoverResources.mockResolvedValue([agentResource("/ws/.agents/agents/worker.md")]);
320
- spies.getCachedFileContent.mockReturnValue(agentMd("worker", "编码"));
321
- const r = await handlers.beforeAgentStart!({ systemPrompt: "" }, createMockCtx());
322
- expect(spies.discoverResources).toHaveBeenCalledTimes(2);
323
- expect(r?.systemPrompt).toContain("<name>worker</name>");
324
- });
325
- });
326
-
327
- // ──────────────────────────────────────────────────────────────
328
- // KV-cache 顺序契约:输出按 name 码点序,重建(两次发现)逐字节一致
329
- // ──────────────────────────────────────────────────────────────
330
-
331
- describe("discoverAllAgents 顺序契约(KV-cache)", () => {
332
- it("输出按 name 码点序排序,与发现层返回顺序(readdir 枚举序)无关", async () => {
333
- const byPath: Record<string, string> = {
334
- "/ws/.agents/agents/zeta.md": agentMd("zeta", "z"),
335
- "/ws/.agents/agents/worker.md": agentMd("worker", "w"),
336
- "/ws/.agents/agents/alpha.md": agentMd("alpha", "a"),
337
- };
338
- // 刻意以非字母序返回(模拟 readdir 无契约枚举序)
339
- spies.discoverResources.mockResolvedValue([
340
- agentResource("/ws/.agents/agents/zeta.md"),
341
- agentResource("/ws/.agents/agents/worker.md"),
342
- agentResource("/ws/.agents/agents/alpha.md"),
343
- ]);
344
- spies.getCachedFileContent.mockImplementation((p: string) => byPath[p] ?? null);
345
-
346
- const { discoverAllAgents } = await import("../subagent-list-injector");
347
- const agents = await discoverAllAgents("/ws");
348
- expect(agents.map((a) => a.name)).toEqual(["alpha", "worker", "zeta"]);
349
- });
350
-
351
- it("重建(两次发现)输出与渲染结果逐字节一致——目录不变时 session_start/fallback/resume 任意重建等价", async () => {
352
- const byPath: Record<string, string> = {
353
- "/ws/.agents/agents/b.md": agentMd("beta", "b"),
354
- "/ws/.agents/agents/a.md": agentMd("alpha", "a"),
355
- };
356
- // 两次发现返回顺序不同(模拟跨进程 readdir 漂移)
357
- spies.discoverResources
358
- .mockResolvedValueOnce([
359
- agentResource("/ws/.agents/agents/b.md"),
360
- agentResource("/ws/.agents/agents/a.md"),
361
- ])
362
- .mockResolvedValueOnce([
363
- agentResource("/ws/.agents/agents/a.md"),
364
- agentResource("/ws/.agents/agents/b.md"),
365
- ]);
366
- spies.getCachedFileContent.mockImplementation((p: string) => byPath[p] ?? null);
367
-
368
- const { discoverAllAgents } = await import("../subagent-list-injector");
369
- const first = await discoverAllAgents("/ws");
370
- const second = await discoverAllAgents("/ws");
371
- expect(second).toEqual(first);
372
- expect(formatAgentList(second, { guide: SUBAGENT_LIST_GUIDE })).toBe(
373
- formatAgentList(first, { guide: SUBAGENT_LIST_GUIDE }),
374
- );
375
- });
376
- });
@@ -1,161 +0,0 @@
1
- // Behavioral tests for weak-model parameter-misuse detectors.
2
- //
3
- // Complements the source-text prompt-quality tests (subagent-tool-prompt.test.ts /
4
- // workflow-tool-prompt.test.ts): those lock that the Correct examples / anti-pattern
5
- // STRINGS exist in source; these lock the actual trigger/no-trigger LOGIC, so a
6
- // refactor that inverts a condition or swaps keys cannot pass just by keeping the
7
- // literal string alive.
8
- //
9
- // Covers the detectors added in the weak-model-robustness PR:
10
- // - workflow findFlattenedArgKeys (args sub-fields flattened to top level — P0)
11
- //
12
- // NOTE: subagent hasFlattenedStartFields detector 已随 wave 3 拍平删除——
13
- // startParam envelope 不再存在,task/slug 平铺到顶层是合法形态,原 detector 无意义。
14
-
15
- import { describe, expect, it } from "vitest";
16
-
17
- import { readFileSync } from "node:fs";
18
- import { join } from "node:path";
19
-
20
- // D9 下沉后权威实现迁至 core args-meta——宿主差异(TOOL_TOP_LEVEL 撞名保护)经
21
- // reservedKeys 注入,与 tool-workflow 调用点保持同一契约。
22
- import {
23
- argKeysFromMeta,
24
- findFlattenedArgKeys,
25
- } from "@zhushanwen/subagent-core/orchestration/args-meta.ts";
26
- import { TOOL_TOP_LEVEL } from "../tool-workflow";
27
- import { parseResourceMeta } from "@zhushanwen/subagent-core";
28
-
29
- // 宿主 reservedKeys 注入(与 tool-workflow ARGS_META_OPTIONS 等值契约)
30
- const OPT = { reservedKeys: TOOL_TOP_LEVEL };
31
-
32
- // 真实参数 meta(@pi-meta parameters——m6 动态数据源,与 TC2 同源防漂移)
33
- function rflMeta() {
34
- const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/review-fix-loop.js"), "utf-8");
35
- const meta = parseResourceMeta(src, "workflow");
36
- if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("rfl meta");
37
- return meta.parameters;
38
- }
39
- function chainMeta() {
40
- const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/chain.js"), "utf-8");
41
- const meta = parseResourceMeta(src, "workflow");
42
- if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("chain meta");
43
- return meta.parameters;
44
- }
45
- const RFL = rflMeta();
46
- const CHAIN = chainMeta();
47
-
48
- describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
49
- it("TC3a: trigger——chain 参数集平铺 task 被识别;跨 workflow 区分(items 非 chain 参数不触发)", () => {
50
- expect(
51
- findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN, OPT),
52
- ).toEqual(["task"]);
53
- // 跨 workflow:items 是 map-reduce 参数非 chain 参数 → 不触发(m6 动态集语义)
54
- expect(
55
- findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN, OPT),
56
- ).toEqual([]);
57
- });
58
-
59
- it("TC3b: 嵌套 args 不触发", () => {
60
- expect(
61
- findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN, OPT),
62
- ).toEqual([]);
63
- });
64
-
65
- it("TC3c: 顶层 + args 内共存不触发(args-排除保留)", () => {
66
- expect(
67
- findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN, OPT),
68
- ).toEqual([]);
69
- });
70
-
71
- it("TC3d: 无已知键不触发;空集不触发", () => {
72
- expect(
73
- findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN, OPT),
74
- ).toEqual([]);
75
- // meta undefined → 空键集(原「空集不触发」等价形态)
76
- expect(
77
- findFlattenedArgKeys({ action: "status" }, undefined),
78
- ).toEqual([]);
79
- });
80
-
81
- it("TC3e: review-fix-loop fixAgent 平铺被识别(动态集 exact)", () => {
82
- expect(
83
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL, OPT),
84
- ).toEqual(["fixAgent"]);
85
- expect(
86
- findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL, OPT),
87
- ).toEqual([]);
88
- });
89
-
90
- it("TC3f: batchN 经 pattern 触发 + batchl 拼错不触发(数字后缀语义)", () => {
91
- expect(
92
- findFlattenedArgKeys(
93
- { action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" },
94
- RFL,
95
- OPT,
96
- ),
97
- ).toEqual(["targetType", "target"]);
98
- expect(
99
- findFlattenedArgKeys(
100
- { action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true },
101
- RFL,
102
- OPT,
103
- ),
104
- ).toEqual(["batch1", "autoCommit"]);
105
- expect(
106
- findFlattenedArgKeys(
107
- { action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } },
108
- RFL,
109
- OPT,
110
- ),
111
- ).toEqual([]);
112
- // batchl 拼错(非数字后缀)不触发(m6 评审 C-1:pattern 自带数字后缀校验)
113
- expect(
114
- findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL, OPT),
115
- ).toEqual([]);
116
- });
117
-
118
- it("TC3g: 收敛参数平铺被识别(动态集 exact——S-13 3 键,model 升格 TOOL_TOP_LEVEL 后排除)", () => {
119
- expect(
120
- findFlattenedArgKeys(
121
- { action: "run", name: "review-fix-loop", model: "ds-flash", maxFixAttempts: 3, convergeNewIssues: 2, convergeRounds: 3 },
122
- RFL,
123
- OPT,
124
- ),
125
- ).toEqual(["maxFixAttempts", "convergeNewIssues", "convergeRounds"]);
126
- expect(
127
- findFlattenedArgKeys(
128
- { action: "run", name: "review-fix-loop", args: { model: "ds-flash", maxFixAttempts: 3 }, convergeRounds: 3 },
129
- RFL,
130
- OPT,
131
- ),
132
- ).toEqual(["convergeRounds"]);
133
- });
134
-
135
- it("TC3h: non-object 输入返回 []", () => {
136
- expect(findFlattenedArgKeys(null)).toEqual([]);
137
- expect(findFlattenedArgKeys(undefined)).toEqual([]);
138
- });
139
-
140
- it("TC3i: tool 顶层键不误报(M-3 回归——合成 parameters 直测 argKeysFromMeta 排除)", () => {
141
- // 合成 parameters:workflow 声明参数 name(tool 键撞名)——argKeysFromMeta 必须排除
142
- const synthetic = {
143
- type: "object",
144
- properties: { name: { type: "string" }, task: { type: "string" } },
145
- required: ["name"],
146
- };
147
- const keys = argKeysFromMeta(synthetic, OPT);
148
- expect(keys.exact.has("task")).toBe(true);
149
- expect(keys.exact.has("name")).toBe(false); // TOOL_TOP_LEVEL 排除(M-3 真回归锁定)
150
- // 合法调用不误报
151
- expect(
152
- findFlattenedArgKeys(
153
- { action: "run", name: "mywf", args: { name: "x" } },
154
- synthetic,
155
- OPT,
156
- ),
157
- ).toEqual([]);
158
- // 真实 meta 不含 name(探针实测 16 键)——附加验证
159
- expect(argKeysFromMeta(RFL, OPT).exact.has("name")).toBe(false);
160
- });
161
- });