@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 +6 -5
- package/package.json +14 -9
- 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 +349 -86
- 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
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
// engine-section-stability.test.ts —— [engine-awareness U4 stability-guard]
|
|
2
|
-
//
|
|
3
|
-
// 字节稳定守护(设计 docs/design/subagent-engine-awareness-injection.md(已删除,git 可追溯)§3.3 D7 /
|
|
4
|
-
// §4 A8 的代码级前置):engine 段延续注入纪律——确定性渲染、链尾位置,引擎切换只断
|
|
5
|
-
// system prompt 尾部 cache 前缀(与 provider models 段变更同判,不新增 cache 破坏面,
|
|
6
|
-
// cache-probe 前缀指纹归因兼容)。
|
|
7
|
-
//
|
|
8
|
-
// 三层覆盖:
|
|
9
|
-
// 1. 段渲染确定性:pi / zcode / ghost 三形态多次独立调用逐字节相等(toBe 级);
|
|
10
|
-
// 2. 引擎切换只变尾部:provider models 段前缀不变,变化只发生在 engine 段位置(尾部);
|
|
11
|
-
// 3. 段序守护:engine 段恒位于 provider models 段之后(恒链尾)。双保险实现:
|
|
12
|
-
// a) handler 链模拟——真实导入 setupModelListInjector + 复刻 engine handler
|
|
13
|
-
// (D7-④ 起为 engine-awareness.ts 的 setupEngineAwarenessInjector)的渲染
|
|
14
|
-
// 拼装(检测编排部分归 engine-awareness.test.ts),模拟 pi 的
|
|
15
|
-
// before_agent_start 链式叠加语义;
|
|
16
|
-
// b) 源码级结构断言——index.ts 锚定注册序(四个 setup 调用先后),engine-awareness.ts
|
|
17
|
-
// 锚定拼装形态。
|
|
18
|
-
//
|
|
19
|
-
// 源码级断言的脆弱性权衡(刻意为之):engine handler 的生产实现装配依赖
|
|
20
|
-
// ModelConfigService 单例与 sessionState 存取器(getModelConfigService() 在测试环境
|
|
21
|
-
// 恒 null,导入后 handler 直接早退),链模拟仍以复刻形态锚定拼装保真;而
|
|
22
|
-
// before_agent_start 多 handler 的段序由注册序唯一决定,「注册序」这一事实只存在于
|
|
23
|
-
// index.ts 源码中——源码结构断言是唯一能锚定它的方式。重构挪动注册位置导致本断言
|
|
24
|
-
// 失败属预期行为(提醒同步更新链模拟与守护锚点),不是误报。
|
|
25
|
-
|
|
26
|
-
import { readFileSync } from "node:fs";
|
|
27
|
-
import { fileURLToPath } from "node:url";
|
|
28
|
-
|
|
29
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
30
|
-
|
|
31
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
32
|
-
|
|
33
|
-
// C5①:formatModelList/ModelEntry 下沉 core barrel(setupModelListInjector 留 injector)
|
|
34
|
-
import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
|
|
35
|
-
import { MODEL_LIST_GUIDE, setupModelListInjector } from "../model-list-injector.ts";
|
|
36
|
-
import type { EnginePort } from "@zhushanwen/subagent-core";
|
|
37
|
-
import { clearEngines, registerEngine } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
|
|
38
|
-
import {
|
|
39
|
-
buildEngineModelsPromptAppend,
|
|
40
|
-
buildSubagentEngineSection,
|
|
41
|
-
} from "@zhushanwen/subagent-core";
|
|
42
|
-
|
|
43
|
-
// ── 测试数据 ────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
/** pi registry 风格条目(provider models 段数据源;同时充当 ctx.modelRegistry 快照)。 */
|
|
46
|
-
const PROVIDER_ENTRIES: ModelEntry[] = [
|
|
47
|
-
{
|
|
48
|
-
provider: "zai-coding-cn",
|
|
49
|
-
id: "glm-5.3",
|
|
50
|
-
name: "GLM 5.3",
|
|
51
|
-
reasoning: true,
|
|
52
|
-
input: ["text"],
|
|
53
|
-
contextWindow: 200_000,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
provider: "minimax-cn",
|
|
57
|
-
id: "MiniMax-M3",
|
|
58
|
-
name: "MiniMax M3",
|
|
59
|
-
reasoning: true,
|
|
60
|
-
input: ["text", "image"],
|
|
61
|
-
contextWindow: 1_000_000,
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
/** zcode 引擎清单(v2 registry 风格 id)。 */
|
|
66
|
-
const ZCODE_MODELS: Array<{ id: string; name?: string }> = [
|
|
67
|
-
{ id: "builtin:bigmodel-coding-plan/GLM-5.3", name: "GLM-5.3" },
|
|
68
|
-
{ id: "builtin:bigmodel-coding-plan/GLM-5.3-Flash" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
/** 模拟 pi 核心 system prompt(engine 追加段之外的全部内容)。 */
|
|
72
|
-
const BASE = "You are a coding agent.\n\n# System\nCore prompt body.";
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* engine handler 的追加段拼装(与 engine-awareness.ts setupEngineAwarenessInjector
|
|
76
|
-
* 内 handler 同款:状态段 + 清单段、空段剔除、\n\n 连接)。复刻保真由下方「段序守护」
|
|
77
|
-
* describe 的源码锚点断言保证。
|
|
78
|
-
*/
|
|
79
|
-
function composeEngineAppend(defaultEngine: string | undefined): string {
|
|
80
|
-
return [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
|
|
81
|
-
.filter((part) => part !== "")
|
|
82
|
-
.join("\n\n");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** index.ts 尾部追加模板的分隔符(`${event.systemPrompt}\n\n${append}`)。 */
|
|
86
|
-
const APPEND_SEPARATOR = "\n\n";
|
|
87
|
-
|
|
88
|
-
/** 合成一个 turn 的 system prompt 尾部:BASE + provider models 段 + engine 追加段。 */
|
|
89
|
-
function composeTurn(defaultEngine: string | undefined): string {
|
|
90
|
-
const providerSection = formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
|
|
91
|
-
const afterProvider = providerSection === "" ? BASE : BASE + providerSection;
|
|
92
|
-
const append = composeEngineAppend(defaultEngine);
|
|
93
|
-
return append === "" ? afterProvider : `${afterProvider}${APPEND_SEPARATOR}${append}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 最大公共前缀长度(逐码元比较;用于「分叉点不早于稳定头部」断言)。 */
|
|
97
|
-
function commonPrefixLength(a: string, b: string): number {
|
|
98
|
-
const n = Math.min(a.length, b.length);
|
|
99
|
-
let i = 0;
|
|
100
|
-
while (i < n && a[i] === b[i]) i++;
|
|
101
|
-
return i;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** 最小 fake 引擎(照 model-prompt.test.ts 同款形态):仅 listModels 参与注入链。 */
|
|
105
|
-
function fakeEngine(id: string, models: Array<{ id: string; name?: string }>): EnginePort {
|
|
106
|
-
return {
|
|
107
|
-
id,
|
|
108
|
-
capabilities: () => ({ conversation: "unsupported", steer: "unsupported", sandbox: "none" }),
|
|
109
|
-
probe: async () => ({ ok: true, engineVersion: "test" }),
|
|
110
|
-
run: async () => {
|
|
111
|
-
throw new Error("not used in this test");
|
|
112
|
-
},
|
|
113
|
-
read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
|
|
114
|
-
// 每次调用返回新数组实例(模拟 registry 每 turn 现值)——渲染必须与实例无关
|
|
115
|
-
listModels: () => models.map((m) => ({ ...m })),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
clearEngines();
|
|
121
|
-
registerEngine("zcode", () => fakeEngine("zcode", ZCODE_MODELS));
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
afterEach(() => {
|
|
125
|
-
clearEngines();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ── 1. 段渲染确定性(D7)────────────────────────────────
|
|
129
|
-
|
|
130
|
-
describe("段渲染确定性(D7:同输入恒同输出)", () => {
|
|
131
|
-
it("pi 形态:状态段多次独立调用逐字节相等;清单段恒空串", () => {
|
|
132
|
-
// pi 形态不依赖注册表(直接走缺省分支),registry 中有 zcode 不影响
|
|
133
|
-
const first = buildSubagentEngineSection("pi");
|
|
134
|
-
const second = buildSubagentEngineSection("pi");
|
|
135
|
-
const third = buildSubagentEngineSection("pi");
|
|
136
|
-
expect(first).toBe(second);
|
|
137
|
-
expect(second).toBe(third);
|
|
138
|
-
// 非空自检:确认 toBe 断言真的在比对段内容而非空串
|
|
139
|
-
expect(first).toContain("<current_subagent_engine>");
|
|
140
|
-
expect(buildEngineModelsPromptAppend("pi")).toBe("");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("zcode 形态:状态段 + 清单段多次独立调用逐字节相等(listModels 每次返回新数组实例)", () => {
|
|
144
|
-
const s1 = buildSubagentEngineSection("zcode");
|
|
145
|
-
const s2 = buildSubagentEngineSection("zcode");
|
|
146
|
-
const m1 = buildEngineModelsPromptAppend("zcode");
|
|
147
|
-
const m2 = buildEngineModelsPromptAppend("zcode");
|
|
148
|
-
expect(s1).toBe(s2);
|
|
149
|
-
expect(m1).toBe(m2);
|
|
150
|
-
expect(m1).toContain("<available_zcode_models>");
|
|
151
|
-
expect(m1).toContain("<id>builtin:bigmodel-coding-plan/GLM-5.3</id>");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("ghost 形态(未注册引擎):警告段多次独立调用逐字节相等", () => {
|
|
155
|
-
const first = buildSubagentEngineSection("ghost");
|
|
156
|
-
const second = buildSubagentEngineSection("ghost");
|
|
157
|
-
expect(first).toBe(second);
|
|
158
|
-
expect(first).toContain("engine 'ghost' is not registered");
|
|
159
|
-
expect(buildEngineModelsPromptAppend("ghost")).toBe("");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("形态间干扰不产生状态残留:zcode → pi → zcode,两次 zcode 输出逐字节相等", () => {
|
|
163
|
-
const before = buildSubagentEngineSection("zcode");
|
|
164
|
-
// 穿插渲染其他形态(含清单段消费 listModels),再回到 zcode
|
|
165
|
-
buildSubagentEngineSection("pi");
|
|
166
|
-
buildEngineModelsPromptAppend("zcode");
|
|
167
|
-
buildSubagentEngineSection("ghost");
|
|
168
|
-
expect(buildSubagentEngineSection("zcode")).toBe(before);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("注册表重建(clearEngines + 同 id 同清单重注册)后同形态输出不变", () => {
|
|
172
|
-
const firstSection = buildSubagentEngineSection("zcode");
|
|
173
|
-
const firstAppend = buildEngineModelsPromptAppend("zcode");
|
|
174
|
-
clearEngines();
|
|
175
|
-
registerEngine("zcode", () => fakeEngine("zcode", ZCODE_MODELS));
|
|
176
|
-
expect(buildSubagentEngineSection("zcode")).toBe(firstSection);
|
|
177
|
-
expect(buildEngineModelsPromptAppend("zcode")).toBe(firstAppend);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("组合拼装(index.ts 同款:状态段 + 清单段 join)同引擎多次调用逐字节相等", () => {
|
|
181
|
-
for (const engine of ["zcode", "pi", "ghost"]) {
|
|
182
|
-
expect(composeEngineAppend(engine)).toBe(composeEngineAppend(engine));
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ── 2. 引擎切换只变尾部(A8 前置)───────────────────────
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 断言「从 from 切到 to 时变化只发生在尾部 engine 段」:
|
|
191
|
-
* - provider models 段及其之前的头部逐字节不变(公共前缀 toBe 级断言);
|
|
192
|
-
* - 剥离尾部 engine 追加段后两串余部逐字节相等且恰为稳定头部;
|
|
193
|
-
* - 分叉点不早于稳定头部末尾(差异绝不侵入 provider models 段);
|
|
194
|
-
* - 切换后的引擎追加段恰居 prompt 尾部。
|
|
195
|
-
*/
|
|
196
|
-
function expectOnlyTailDiffers(from: string | undefined, to: string | undefined): void {
|
|
197
|
-
const stableHead = BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
|
|
198
|
-
const before = composeTurn(from);
|
|
199
|
-
const after = composeTurn(to);
|
|
200
|
-
const beforeAppend = composeEngineAppend(from);
|
|
201
|
-
const afterAppend = composeEngineAppend(to);
|
|
202
|
-
|
|
203
|
-
expect(before).not.toBe(after);
|
|
204
|
-
expect(before.startsWith(stableHead)).toBe(true);
|
|
205
|
-
expect(after.startsWith(stableHead)).toBe(true);
|
|
206
|
-
expect(before.slice(0, before.length - beforeAppend.length - APPEND_SEPARATOR.length)).toBe(
|
|
207
|
-
stableHead,
|
|
208
|
-
);
|
|
209
|
-
expect(after.slice(0, after.length - afterAppend.length - APPEND_SEPARATOR.length)).toBe(
|
|
210
|
-
stableHead,
|
|
211
|
-
);
|
|
212
|
-
expect(commonPrefixLength(before, after)).toBeGreaterThanOrEqual(stableHead.length);
|
|
213
|
-
expect(after.endsWith(afterAppend)).toBe(true);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
describe("引擎切换只变尾部(A8 前置:变化只断尾部 cache 前缀)", () => {
|
|
217
|
-
it("zcode → pi:provider models 段前缀不变,变化只发生在 engine 段(尾部)", () => {
|
|
218
|
-
expectOnlyTailDiffers("zcode", "pi");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("pi → zcode(反向切换):同款只变尾部", () => {
|
|
222
|
-
expectOnlyTailDiffers("pi", "zcode");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("zcode → ghost(配置手误形态):G4 降级段同样只替换尾部,不破坏前缀", () => {
|
|
226
|
-
expectOnlyTailDiffers("zcode", "ghost");
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ── 3. 段序守护:engine 段恒链尾 ────────────────────────
|
|
231
|
-
|
|
232
|
-
/** 链模拟用 handler 形态(参数收窄到运行时实际消费的字段)。 */
|
|
233
|
-
type ChainHandler = (event: { systemPrompt: string }, ctx: unknown) => unknown;
|
|
234
|
-
|
|
235
|
-
/** fake ExtensionAPI:捕获 pi.on 注册的 handler(保持注册序)。 */
|
|
236
|
-
function capturePi(): { handlers: ChainHandler[]; api: ExtensionAPI } {
|
|
237
|
-
const handlers: ChainHandler[] = [];
|
|
238
|
-
const api = {
|
|
239
|
-
on: (_event: string, handler: (...args: unknown[]) => unknown) => {
|
|
240
|
-
// on 的 mock 签名参数为 unknown[],收窄到两参形态(pi 调用约定恒两参)
|
|
241
|
-
handlers.push(((event: unknown, ctx: unknown) => handler(event, ctx)) as ChainHandler);
|
|
242
|
-
},
|
|
243
|
-
} as unknown as ExtensionAPI;
|
|
244
|
-
return { handlers, api };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** 模拟 pi 的 before_agent_start 串联语义:按注册序执行,前序返回的 systemPrompt 作为后序输入。 */
|
|
248
|
-
async function runHandlerChain(handlers: ChainHandler[], initialPrompt: string): Promise<string> {
|
|
249
|
-
let prompt = initialPrompt;
|
|
250
|
-
const ctx = { modelRegistry: { getAvailable: () => PROVIDER_ENTRIES } };
|
|
251
|
-
for (const handler of handlers) {
|
|
252
|
-
const result: unknown = await handler({ systemPrompt: prompt }, ctx);
|
|
253
|
-
// 运行时收窄:handler 返回 { systemPrompt: string } 时替换链上 prompt,否则保持
|
|
254
|
-
if (typeof result === "object" && result !== null && "systemPrompt" in result) {
|
|
255
|
-
const sp: unknown = (result as { systemPrompt: unknown }).systemPrompt;
|
|
256
|
-
if (typeof sp === "string") prompt = sp;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return prompt;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
describe("段序守护:engine 段恒链尾(D7)", () => {
|
|
263
|
-
it("handler 链模拟:按 index.ts 注册序执行后,engine 段位于 provider models 段之后且居尾", async () => {
|
|
264
|
-
const { handlers, api } = capturePi();
|
|
265
|
-
// 真实导入 model list injector(provider models 段的生产渲染路径)
|
|
266
|
-
setupModelListInjector(api);
|
|
267
|
-
// 复刻 engine handler(engine-awareness.ts setupEngineAwarenessInjector)的渲染
|
|
268
|
-
// 拼装部分(检测编排部分归 engine-awareness.test.ts)
|
|
269
|
-
const defaultEngine = "zcode";
|
|
270
|
-
handlers.push((event) => {
|
|
271
|
-
const append = composeEngineAppend(defaultEngine);
|
|
272
|
-
return { systemPrompt: `${event.systemPrompt}${APPEND_SEPARATOR}${append}` };
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const prompt = await runHandlerChain(handlers, BASE);
|
|
276
|
-
|
|
277
|
-
const providerIdx = prompt.indexOf("<available_provider_models>");
|
|
278
|
-
const statusIdx = prompt.indexOf("<current_subagent_engine>");
|
|
279
|
-
const zcodeListIdx = prompt.indexOf("<available_zcode_models>");
|
|
280
|
-
expect(providerIdx).toBeGreaterThanOrEqual(0);
|
|
281
|
-
// 段序硬约束:engine 状态段与引擎清单段都在 provider models 段之后
|
|
282
|
-
expect(statusIdx).toBeGreaterThan(providerIdx);
|
|
283
|
-
expect(zcodeListIdx).toBeGreaterThan(providerIdx);
|
|
284
|
-
// 恒链尾:engine 追加段是 prompt 的最后内容
|
|
285
|
-
expect(prompt.endsWith(`${APPEND_SEPARATOR}${composeEngineAppend(defaultEngine)}`)).toBe(true);
|
|
286
|
-
// model list handler 注入形态锚定(BASE + injection 无分隔,生产 handler 同款)
|
|
287
|
-
expect(prompt.startsWith(BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE }))).toBe(true);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("对照:注册序颠倒(engine 先、model list 后)时段序断言必失败——守护有判别力", async () => {
|
|
291
|
-
const { handlers, api } = capturePi();
|
|
292
|
-
const defaultEngine = "zcode";
|
|
293
|
-
handlers.push((event) => {
|
|
294
|
-
const append = composeEngineAppend(defaultEngine);
|
|
295
|
-
return { systemPrompt: `${event.systemPrompt}${APPEND_SEPARATOR}${append}` };
|
|
296
|
-
});
|
|
297
|
-
setupModelListInjector(api);
|
|
298
|
-
|
|
299
|
-
const prompt = await runHandlerChain(handlers, BASE);
|
|
300
|
-
|
|
301
|
-
// 颠倒后 engine 段跑到 provider models 段之前——证明上一条 it 的断言非平凡
|
|
302
|
-
const providerIdx = prompt.indexOf("<available_provider_models>");
|
|
303
|
-
const statusIdx = prompt.indexOf("<current_subagent_engine>");
|
|
304
|
-
expect(statusIdx).toBeGreaterThanOrEqual(0);
|
|
305
|
-
expect(statusIdx).toBeLessThan(providerIdx);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it("源码级守护:engine handler 注册序在 model list injector 之后(段序由注册序唯一决定)", () => {
|
|
309
|
-
const src = readFileSync(fileURLToPath(new URL("../../index.ts", import.meta.url)), "utf8");
|
|
310
|
-
const eaSrc = readFileSync(fileURLToPath(new URL("../engine-awareness.ts", import.meta.url)), "utf8");
|
|
311
|
-
const lineOf = (needle: string): number => {
|
|
312
|
-
const idx = src.indexOf(needle);
|
|
313
|
-
if (idx < 0) throw new Error(`index.ts 中未找到守护锚点:${needle}`);
|
|
314
|
-
return src.slice(0, idx).split("\n").length;
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
// 注册序:subagents → workflows → provider models → engine handler(链尾,D7)。
|
|
318
|
-
// D7-④:index.ts 不再有直接 pi.on("before_agent_start") 注册——四段注入全部经
|
|
319
|
-
// setup* 函数(engine 段经 setupEngineAwarenessInjector)。「零直接注册」断言让
|
|
320
|
-
// 「链尾」语义精确成立;未来若新增直接注册需人工复核段序。
|
|
321
|
-
expect(src.split('pi.on("before_agent_start"')).toHaveLength(1);
|
|
322
|
-
const lineSubagents = lineOf("setupSubagentListInjector(pi);");
|
|
323
|
-
const lineWorkflows = lineOf("setupWorkflowListInjector(pi);");
|
|
324
|
-
const lineModels = lineOf("setupModelListInjector(pi);");
|
|
325
|
-
const lineEngineSetup = lineOf("setupEngineAwarenessInjector(pi,");
|
|
326
|
-
expect(lineSubagents).toBeLessThan(lineWorkflows);
|
|
327
|
-
expect(lineWorkflows).toBeLessThan(lineModels);
|
|
328
|
-
expect(lineModels).toBeLessThan(lineEngineSetup);
|
|
329
|
-
|
|
330
|
-
// engine handler 的唯一注册点在 engine-awareness.ts(setupEngineAwarenessInjector
|
|
331
|
-
// 内恰一处 pi.on("before_agent_start"))。
|
|
332
|
-
expect(eaSrc.split('pi.on("before_agent_start"')).toHaveLength(2);
|
|
333
|
-
|
|
334
|
-
// engine handler 内渲染调用序:状态段在清单段之前(append 内段序:状态段 → 清单段)。
|
|
335
|
-
// 两个调用在 engine-awareness.ts 中位于同一行(数组字面量),行号无法区分先后——
|
|
336
|
-
// 用字符偏移断言
|
|
337
|
-
const offsetStatusCall = eaSrc.indexOf("buildSubagentEngineSection(defaultEngine)");
|
|
338
|
-
const offsetModelsCall = eaSrc.indexOf("buildEngineModelsPromptAppend(defaultEngine)");
|
|
339
|
-
if (offsetStatusCall < 0 || offsetModelsCall < 0) {
|
|
340
|
-
throw new Error("engine-awareness.ts 中未找到 engine 段渲染调用锚点");
|
|
341
|
-
}
|
|
342
|
-
expect(offsetStatusCall).toBeGreaterThan(eaSrc.indexOf('pi.on("before_agent_start"'));
|
|
343
|
-
expect(offsetStatusCall).toBeLessThan(offsetModelsCall);
|
|
344
|
-
|
|
345
|
-
// 链模拟复刻保真锚点:append 拼装形态(空段剔除 + \n\n 连接 + 尾部追加模板)。
|
|
346
|
-
// 这些锚点保证 composeEngineAppend 与生产拼装(engine-awareness.ts)的一致性——
|
|
347
|
-
// 生产形态变化会先在这里红,提醒同步更新本文件全部复刻点。
|
|
348
|
-
expect(eaSrc).toContain('[buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]');
|
|
349
|
-
expect(eaSrc).toContain('.filter((part) => part !== "")');
|
|
350
|
-
expect(eaSrc).toContain('.join("\\n\\n")');
|
|
351
|
-
expect(eaSrc).toContain("${event.systemPrompt}\\n\\n${append}");
|
|
352
|
-
});
|
|
353
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
// model-list-injector 单测
|
|
2
|
-
//
|
|
3
|
-
// 覆盖(与 workflow-list-injector.test.ts 对称,但本 injector 无文件发现/模块级
|
|
4
|
-
// 缓存,纯函数比重更大):
|
|
5
|
-
// 1. formatModelList:排序稳定性(registry 返回顺序不保证,输出必须字节稳定)、
|
|
6
|
-
// caps 推导(reasoning/vision)、空 caps 省略、XML 转义、空列表不注入
|
|
7
|
-
// 2. setupModelListInjector handler:注入 append、空列表返回 undefined、
|
|
8
|
-
// registry 异常不阻断(fail-safe)
|
|
9
|
-
|
|
10
|
-
import { describe, expect, it, vi } from "vitest";
|
|
11
|
-
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
|
|
14
|
-
// C5①:formatModelList + ModelEntry 口径下沉 core barrel——测试改从 barrel 取实现,
|
|
15
|
-
// guide 用 pi 宿主注入文案
|
|
16
|
-
import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
|
|
17
|
-
import { MODEL_LIST_GUIDE } from "../model-list-injector";
|
|
18
|
-
|
|
19
|
-
// ── 测试数据工厂 ────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
function entry(overrides: Partial<ModelEntry> = {}): ModelEntry {
|
|
22
|
-
return {
|
|
23
|
-
provider: "zai-coding-cn",
|
|
24
|
-
id: "glm-5.2",
|
|
25
|
-
name: "GLM 5.2",
|
|
26
|
-
reasoning: true,
|
|
27
|
-
input: ["text"],
|
|
28
|
-
contextWindow: 200_000,
|
|
29
|
-
...overrides,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── 纯函数:formatModelList ─────────────────────────────
|
|
34
|
-
|
|
35
|
-
describe("formatModelList", () => {
|
|
36
|
-
it("空列表返回空串(不注入)", () => {
|
|
37
|
-
expect(formatModelList([], { guide: MODEL_LIST_GUIDE })).toBe("");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("渲染 provider/modelId + name + caps + contextWindow", () => {
|
|
41
|
-
const out = formatModelList([
|
|
42
|
-
entry({ provider: "p1", id: "m1", name: "Model One" }),
|
|
43
|
-
], { guide: MODEL_LIST_GUIDE });
|
|
44
|
-
expect(out).toContain("<available_provider_models>");
|
|
45
|
-
expect(out).toContain("<model><id>p1/m1</id><name>Model One</name><caps>reasoning</caps><contextWindow>200000</contextWindow></model>");
|
|
46
|
-
expect(out).toContain("</available_provider_models>");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("input 含 image 时 caps 加 vision;无能力时省略 caps 段", () => {
|
|
50
|
-
const out = formatModelList([
|
|
51
|
-
entry({ id: "vision-m", input: ["text", "image"] }),
|
|
52
|
-
entry({ id: "plain-m", reasoning: false }),
|
|
53
|
-
], { guide: MODEL_LIST_GUIDE });
|
|
54
|
-
expect(out).toContain("<id>zai-coding-cn/vision-m</id>");
|
|
55
|
-
expect(out).toContain("<caps>reasoning,vision</caps>");
|
|
56
|
-
// plain-m:reasoning=false 且无 image → 无 caps 段
|
|
57
|
-
expect(out).toMatch(/<id>zai-coding-cn\/plain-m<\/id><name>[^<]*<\/name><contextWindow>/);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("按 (provider, id) 排序——输入乱序输出仍字节稳定(KV cache 前提)", () => {
|
|
61
|
-
const models = [
|
|
62
|
-
entry({ provider: "b-prov", id: "z-model" }),
|
|
63
|
-
entry({ provider: "a-prov", id: "y-model" }),
|
|
64
|
-
entry({ provider: "a-prov", id: "x-model" }),
|
|
65
|
-
];
|
|
66
|
-
const out1 = formatModelList([...models], { guide: MODEL_LIST_GUIDE });
|
|
67
|
-
const out2 = formatModelList([...models].reverse(), { guide: MODEL_LIST_GUIDE });
|
|
68
|
-
expect(out1).toBe(out2);
|
|
69
|
-
// 排序断言:a-prov/x-model 在最前,b-prov 在最后
|
|
70
|
-
const ids = [...out1.matchAll(/<id>([^<]+)<\/id>/g)].map((m) => m[1]);
|
|
71
|
-
expect(ids).toEqual(["a-prov/x-model", "a-prov/y-model", "b-prov/z-model"]);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("排序为码点序而非 locale 序(跨环境字节一致契约,禁 localeCompare)", () => {
|
|
75
|
-
// 判别样本:码点序 "B"(0x42) < "a"(0x61),而多数 locale 的 localeCompare
|
|
76
|
-
// 会把 "a-model" 排在 "B-model" 前——本断言在 localeCompare 实现下必挂
|
|
77
|
-
const out = formatModelList([
|
|
78
|
-
entry({ provider: "p", id: "a-model" }),
|
|
79
|
-
entry({ provider: "p", id: "B-model" }),
|
|
80
|
-
], { guide: MODEL_LIST_GUIDE });
|
|
81
|
-
const ids = [...out.matchAll(/<id>([^<]+)<\/id>/g)].map((m) => m[1]);
|
|
82
|
-
expect(ids).toEqual(["p/B-model", "p/a-model"]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("name 含 XML 特殊字符时转义(防注入段结构破坏)", () => {
|
|
86
|
-
const out = formatModelList([
|
|
87
|
-
entry({ provider: "p", id: "m", name: `A<&>"'B` }),
|
|
88
|
-
], { guide: MODEL_LIST_GUIDE });
|
|
89
|
-
expect(out).toContain("<name>A<&>"'B</name>");
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ── handler 行为:setupModelListInjector ────────────────
|
|
94
|
-
|
|
95
|
-
describe("setupModelListInjector", () => {
|
|
96
|
-
type Handler = (event: unknown, ctx: unknown) => Promise<unknown>;
|
|
97
|
-
|
|
98
|
-
async function setupWithRegistry(models: ModelEntry[], fail = false): Promise<Handler> {
|
|
99
|
-
const mod = await import("../model-list-injector"); // handler 注册面(含 MODEL_LIST_GUIDE 注入)
|
|
100
|
-
const handlers: Record<string, Handler> = {};
|
|
101
|
-
const pi = {
|
|
102
|
-
on: (name: string, fn: Handler) => {
|
|
103
|
-
handlers[name] = fn;
|
|
104
|
-
},
|
|
105
|
-
} as unknown as ExtensionAPI;
|
|
106
|
-
mod.setupModelListInjector(pi);
|
|
107
|
-
const registry = fail
|
|
108
|
-
? { getAvailable: vi.fn(() => { throw new Error("registry boom"); }) }
|
|
109
|
-
: { getAvailable: vi.fn(() => models) };
|
|
110
|
-
const handler = handlers["before_agent_start"];
|
|
111
|
-
if (!handler) throw new Error("before_agent_start handler not registered");
|
|
112
|
-
return (event: unknown) => handler(event, { modelRegistry: registry });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
it("注册 before_agent_start handler,注入 append 到 systemPrompt 尾部", async () => {
|
|
116
|
-
const handler = await setupWithRegistry([entry()]);
|
|
117
|
-
const result = (await handler({ systemPrompt: "BASE" }, {})) as {
|
|
118
|
-
systemPrompt: string;
|
|
119
|
-
};
|
|
120
|
-
expect(result.systemPrompt.startsWith("BASE")).toBe(true);
|
|
121
|
-
expect(result.systemPrompt).toContain("<available_provider_models>");
|
|
122
|
-
expect(result.systemPrompt).toContain("zai-coding-cn/glm-5.2");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("空模型列表返回 undefined(不返回 systemPrompt,不干预链)", async () => {
|
|
126
|
-
const handler = await setupWithRegistry([]);
|
|
127
|
-
const result = await handler({ systemPrompt: "BASE" }, {});
|
|
128
|
-
expect(result).toBeUndefined();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("registry 异常被吞掉(fail-safe,不阻断 agent turn)", async () => {
|
|
132
|
-
const handler = await setupWithRegistry([], true);
|
|
133
|
-
const result = await handler({ systemPrompt: "BASE" }, {});
|
|
134
|
-
expect(result).toBeUndefined();
|
|
135
|
-
});
|
|
136
|
-
});
|