@zhushanwen/pi-goal 0.3.0 → 0.4.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 +13 -12
- package/package.json +1 -1
- package/src/__tests__/command-adapter.test.ts +404 -0
- package/src/__tests__/deserialize-state.test.ts +65 -94
- package/src/__tests__/event-adapter.test.ts +410 -0
- package/src/__tests__/goal-control-adapter.test.ts +303 -0
- package/src/__tests__/index.test.ts +119 -0
- package/src/__tests__/service.test.ts +317 -0
- package/src/__tests__/session.test.ts +220 -0
- package/src/__tests__/stale-checker.test.ts +78 -0
- package/src/__tests__/stubs/pi-sdk.ts +5 -0
- package/src/adapters/command-adapter.ts +360 -0
- package/src/adapters/event-adapter.ts +17 -0
- package/src/adapters/event-handlers/agent-end.ts +177 -0
- package/src/adapters/event-handlers/agent-start.ts +17 -0
- package/src/adapters/event-handlers/before-agent-start.ts +157 -0
- package/src/adapters/event-handlers/message-end.ts +37 -0
- package/src/adapters/event-handlers/session-start.ts +24 -0
- package/src/adapters/event-handlers/shared.ts +46 -0
- package/src/adapters/event-handlers/turn-end.ts +37 -0
- package/src/adapters/goal-control-adapter.ts +296 -0
- package/src/adapters/ports.ts +89 -0
- package/src/commands.ts +8 -23
- package/src/constants.ts +1 -8
- package/src/engine/__tests__/budget.test.ts +182 -0
- package/src/engine/__tests__/goal.test.ts +126 -0
- package/src/engine/budget.ts +146 -0
- package/src/engine/goal.ts +62 -0
- package/src/engine/types.ts +80 -0
- package/src/index.ts +110 -355
- package/src/persistence.ts +83 -0
- package/src/ports.ts +68 -0
- package/src/projection/__tests__/prompts.test.ts +245 -0
- package/src/projection/__tests__/widget.test.ts +255 -0
- package/src/projection/prompts.ts +250 -0
- package/src/projection/widget.ts +254 -0
- package/src/service.ts +276 -0
- package/src/session.ts +114 -0
- package/src/__tests__/is-task-done.test.ts +0 -43
- package/src/__tests__/validate-update-tasks.test.ts +0 -172
- package/src/action-handlers.ts +0 -412
- package/src/agent-end-handler.ts +0 -306
- package/src/before-agent-start-handler.ts +0 -180
- package/src/budget.ts +0 -159
- package/src/command-handler.ts +0 -299
- package/src/state.ts +0 -274
- package/src/templates.ts +0 -274
- package/src/tool-handler.ts +0 -291
- package/src/widget.ts +0 -225
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* event-adapter.ts 测试 — agent_end / before_agent_start 核心逻辑
|
|
3
|
+
*
|
|
4
|
+
* 覆盖 agent_end 的 continuation + ESC 守卫(FR-6.7)+
|
|
5
|
+
* before_agent_start 的 AUTO_CLEAR/context wrap-up。
|
|
6
|
+
*
|
|
7
|
+
* 全解耦后不再测 allTasksDone followUp(原依赖 pi.__todoGetList,已删)。
|
|
8
|
+
* agent_end 流程简化为:budget 检查 → continuation 去抖。
|
|
9
|
+
*
|
|
10
|
+
* 用 fake pi + fake ctx(不 import Pi SDK)。handler 签名 (pi, session, ctx) → void/result。
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { describe, expect, it, vi } from "vitest";
|
|
14
|
+
|
|
15
|
+
import { handleAgentEnd } from "../adapters/event-handlers/agent-end";
|
|
16
|
+
import { handleBeforeAgentStart } from "../adapters/event-handlers/before-agent-start";
|
|
17
|
+
import { createGoalState } from "../engine/goal";
|
|
18
|
+
import type { GoalRuntimeState } from "../engine/types";
|
|
19
|
+
import { createGoalSession } from "../session";
|
|
20
|
+
|
|
21
|
+
// ── Fake pi / ctx ────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
interface RecordedCall {
|
|
24
|
+
kind: "appendState" | "appendHistory" | "notify" | "sendContext" | "sendUser" | "setStatus" | "setWidget";
|
|
25
|
+
payload?: unknown;
|
|
26
|
+
text?: string;
|
|
27
|
+
level?: string;
|
|
28
|
+
content?: string;
|
|
29
|
+
deliverAs?: string;
|
|
30
|
+
customType?: string;
|
|
31
|
+
key?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeFakePi(): { pi: ExtensionAPI; calls: RecordedCall[]; states: unknown[]; history: unknown[] } {
|
|
35
|
+
const calls: RecordedCall[] = [];
|
|
36
|
+
const states: unknown[] = [];
|
|
37
|
+
const history: unknown[] = [];
|
|
38
|
+
const pi = {
|
|
39
|
+
appendEntry(customType: string, data?: unknown): void {
|
|
40
|
+
calls.push({ kind: customType === "goal-history" ? "appendHistory" : "appendState", payload: data });
|
|
41
|
+
if (customType === "goal-history") history.push(data);
|
|
42
|
+
else states.push(data);
|
|
43
|
+
},
|
|
44
|
+
sendMessage(message: unknown, _options?: unknown): void {
|
|
45
|
+
const msg = message as { customType?: string; content?: string; display?: boolean };
|
|
46
|
+
calls.push({
|
|
47
|
+
kind: "sendContext",
|
|
48
|
+
content: msg.content,
|
|
49
|
+
customType: msg.customType,
|
|
50
|
+
payload: _options,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
sendUserMessage(content: string | unknown[], options?: unknown): void {
|
|
54
|
+
calls.push({
|
|
55
|
+
kind: "sendUser",
|
|
56
|
+
content: typeof content === "string" ? content : undefined,
|
|
57
|
+
payload: options,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
} as unknown as ExtensionAPI;
|
|
61
|
+
return { pi, calls, states, history };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeFakeCtx(overrides?: {
|
|
65
|
+
aborted?: boolean;
|
|
66
|
+
hasUI?: boolean;
|
|
67
|
+
contextUsage?: { tokens?: number; contextWindow?: number };
|
|
68
|
+
}): { ctx: ExtensionContext; calls: RecordedCall[] } {
|
|
69
|
+
const calls: RecordedCall[] = [];
|
|
70
|
+
const signal = { aborted: overrides?.aborted ?? false } as AbortSignal;
|
|
71
|
+
const ctx = {
|
|
72
|
+
hasUI: overrides?.hasUI ?? true,
|
|
73
|
+
signal,
|
|
74
|
+
getContextUsage: () => overrides?.contextUsage,
|
|
75
|
+
ui: {
|
|
76
|
+
notify: (text: string, level: string) => {
|
|
77
|
+
calls.push({ kind: "notify", text, level });
|
|
78
|
+
},
|
|
79
|
+
setStatus: (key: string, text: string | undefined) => {
|
|
80
|
+
calls.push({ kind: "setStatus", key, text });
|
|
81
|
+
},
|
|
82
|
+
setWidget: (key: string, _content: unknown) => {
|
|
83
|
+
calls.push({ kind: "setWidget", key });
|
|
84
|
+
},
|
|
85
|
+
theme: {
|
|
86
|
+
fg: (_color: string, text: string) => text,
|
|
87
|
+
bold: (text: string) => text,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
sessionManager: { getEntries: () => [], getBranch: () => undefined },
|
|
91
|
+
} as unknown as ExtensionContext;
|
|
92
|
+
return { ctx, calls };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function allCalls(piCalls: RecordedCall[], ctxCalls: RecordedCall[]): RecordedCall[] {
|
|
96
|
+
return [...piCalls, ...ctxCalls];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── 辅助:构造 state ──────────────────────────────────
|
|
100
|
+
|
|
101
|
+
function makeRunningState(overrides?: Partial<GoalRuntimeState>): GoalRuntimeState {
|
|
102
|
+
return {
|
|
103
|
+
...createGoalState("test objective"),
|
|
104
|
+
timeStartedAt: 0, // 关闭时间累计(避免测试随机性)
|
|
105
|
+
...overrides,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── handleAgentEnd:ESC 守卫 ──────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("handleAgentEnd — FR-6.7 ESC 守卫", () => {
|
|
112
|
+
it("aborted=true:不发 continuation、不递增 stall、不做 budget 检查、goal 保持 active", async () => {
|
|
113
|
+
const { pi, calls: piCalls, states } = makeFakePi();
|
|
114
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
|
|
115
|
+
const session = createGoalSession();
|
|
116
|
+
session.state = makeRunningState({ tokensUsed: 1000, lastTurnTokensUsed: 0 });
|
|
117
|
+
|
|
118
|
+
await handleAgentEnd(pi, session, ctx);
|
|
119
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
120
|
+
|
|
121
|
+
// 不发 continuation(无 sendContext/sendUser)
|
|
122
|
+
expect(all.filter((c) => c.kind === "sendContext" || c.kind === "sendUser")).toHaveLength(0);
|
|
123
|
+
// 不做 budget 检查(无 notify 关于 budget)
|
|
124
|
+
expect(all.filter((c) => c.kind === "notify")).toHaveLength(0);
|
|
125
|
+
// goal 保持 active(status 不变)
|
|
126
|
+
expect(session.state!.status).toBe("active");
|
|
127
|
+
// 不 persist(ESC 不触发副作用)
|
|
128
|
+
expect(states).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("aborted=true + 终态:仍走终态 notify(ESC 不影响终态路径)", async () => {
|
|
132
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
133
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx({ aborted: true });
|
|
134
|
+
const session = createGoalSession();
|
|
135
|
+
session.state = makeRunningState({ status: "complete" });
|
|
136
|
+
|
|
137
|
+
await handleAgentEnd(pi, session, ctx);
|
|
138
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
139
|
+
|
|
140
|
+
// 终态 notify 仍触发
|
|
141
|
+
const notify = all.filter((c) => c.kind === "notify");
|
|
142
|
+
expect(notify).toHaveLength(1);
|
|
143
|
+
expect(notify[0]!.text).toContain("Objective completed");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── handleAgentEnd:continuation(去抖 + 正常发送)──────
|
|
148
|
+
|
|
149
|
+
describe("handleAgentEnd — continuation", () => {
|
|
150
|
+
it("continuation 去抖:tokenDelta=0(空 turn)不发", async () => {
|
|
151
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
152
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx();
|
|
153
|
+
const session = createGoalSession();
|
|
154
|
+
session.state = makeRunningState({
|
|
155
|
+
tokensUsed: 100,
|
|
156
|
+
lastTurnTokensUsed: 100, // tokenDelta = 0
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await handleAgentEnd(pi, session, ctx);
|
|
160
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
161
|
+
|
|
162
|
+
const sendContext = all.filter((c) => c.kind === "sendContext");
|
|
163
|
+
expect(sendContext).toHaveLength(0); // 空 turn 不发
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("continuation 正常:tokenDelta>0 → 发 continuationPrompt", async () => {
|
|
167
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
168
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx();
|
|
169
|
+
const session = createGoalSession();
|
|
170
|
+
session.state = makeRunningState({
|
|
171
|
+
tokensUsed: 200,
|
|
172
|
+
lastTurnTokensUsed: 0, // tokenDelta = 200 > 0
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await handleAgentEnd(pi, session, ctx);
|
|
176
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
177
|
+
|
|
178
|
+
const sendContext = all.filter((c) => c.kind === "sendContext");
|
|
179
|
+
expect(sendContext).toHaveLength(1);
|
|
180
|
+
expect(sendContext[0]!.content).toContain("[GOAL]");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── handleAgentEnd:并发保护(G-021)+ stale(G-020)───
|
|
185
|
+
|
|
186
|
+
describe("handleAgentEnd — 并发保护 + stale 快照", () => {
|
|
187
|
+
it("isProcessing=true → 直接返回(防重入)", async () => {
|
|
188
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
189
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx();
|
|
190
|
+
const session = createGoalSession();
|
|
191
|
+
session.state = makeRunningState();
|
|
192
|
+
session.isProcessing = true; // 已锁
|
|
193
|
+
|
|
194
|
+
await handleAgentEnd(pi, session, ctx);
|
|
195
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
196
|
+
|
|
197
|
+
// 无任何副作用
|
|
198
|
+
expect(all).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("finally 块释放 isProcessing(即使中途 return)", async () => {
|
|
202
|
+
const { pi } = makeFakePi();
|
|
203
|
+
const { ctx } = makeFakeCtx({ aborted: true });
|
|
204
|
+
const session = createGoalSession();
|
|
205
|
+
session.state = makeRunningState();
|
|
206
|
+
|
|
207
|
+
await handleAgentEnd(pi, session, ctx);
|
|
208
|
+
|
|
209
|
+
expect(session.isProcessing).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("state=null → 直接返回(无 goal 时)", async () => {
|
|
213
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
214
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx();
|
|
215
|
+
const session = createGoalSession();
|
|
216
|
+
// session.state 保持 null
|
|
217
|
+
|
|
218
|
+
await handleAgentEnd(pi, session, ctx);
|
|
219
|
+
|
|
220
|
+
expect(allCalls(piCalls, ctxCalls)).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── handleBeforeAgentStart ────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("handleBeforeAgentStart", () => {
|
|
227
|
+
it("state=null → 返回 undefined", async () => {
|
|
228
|
+
const { pi } = makeFakePi();
|
|
229
|
+
const { ctx } = makeFakeCtx();
|
|
230
|
+
const session = createGoalSession();
|
|
231
|
+
|
|
232
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
233
|
+
|
|
234
|
+
expect(result).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("正常 active goal → 返回 context injection 消息", async () => {
|
|
238
|
+
const { pi } = makeFakePi();
|
|
239
|
+
const { ctx } = makeFakeCtx();
|
|
240
|
+
const session = createGoalSession();
|
|
241
|
+
session.state = makeRunningState();
|
|
242
|
+
|
|
243
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
244
|
+
|
|
245
|
+
expect(result).toBeDefined();
|
|
246
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
247
|
+
expect(result!.message.content).toContain("[GOAL mode activated]");
|
|
248
|
+
expect(result!.message.display).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// 全解耦后:planAvailable 恒 true(不再运行时探测 pi.__planStart)。
|
|
252
|
+
// contextInjectionPrompt 恒定注入 plan mode 建议段落,AI 自行判断是否用。
|
|
253
|
+
it("恒定注入 plan mode 建议段落(全解耦:不再探测 plan extension)", async () => {
|
|
254
|
+
const { pi } = makeFakePi();
|
|
255
|
+
const { ctx } = makeFakeCtx();
|
|
256
|
+
const session = createGoalSession();
|
|
257
|
+
session.state = makeRunningState();
|
|
258
|
+
|
|
259
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
260
|
+
|
|
261
|
+
expect(result).toBeDefined();
|
|
262
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
263
|
+
expect(result!.message.content).toContain("plan mode");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// FR-8.1 G-007: AUTO_CLEAR_TURNS=2
|
|
267
|
+
it("终态 + turnsInTerminal >= 2 → clearGoalSession(state 清空)", async () => {
|
|
268
|
+
const { pi } = makeFakePi();
|
|
269
|
+
const { ctx } = makeFakeCtx();
|
|
270
|
+
const session = createGoalSession();
|
|
271
|
+
session.state = makeRunningState({
|
|
272
|
+
status: "complete",
|
|
273
|
+
currentTurnIndex: 10,
|
|
274
|
+
completedAtTurnIndex: 8, // turnsInTerminal = 2 >= AUTO_CLEAR_TURNS
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
278
|
+
|
|
279
|
+
expect(result).toBeUndefined();
|
|
280
|
+
expect(session.state).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("终态 + turnsInTerminal < 2 → 不清理,折叠 status bar", async () => {
|
|
284
|
+
const { pi, calls: piCalls } = makeFakePi();
|
|
285
|
+
const { ctx, calls: ctxCalls } = makeFakeCtx({ hasUI: true });
|
|
286
|
+
const session = createGoalSession();
|
|
287
|
+
session.state = makeRunningState({
|
|
288
|
+
status: "complete",
|
|
289
|
+
currentTurnIndex: 10,
|
|
290
|
+
completedAtTurnIndex: 9, // turnsInTerminal = 1 < 2
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
294
|
+
const all = allCalls(piCalls, ctxCalls);
|
|
295
|
+
|
|
296
|
+
expect(result).toBeUndefined();
|
|
297
|
+
expect(session.state).not.toBeNull(); // 未清理
|
|
298
|
+
// setStatus 触发(折叠为终态单行)
|
|
299
|
+
expect(all.filter((c) => c.kind === "setStatus" && c.key === "goal").length).toBeGreaterThan(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ADR-002 context usage 提示(CONTEXT_USAGE_RATIO_LIMIT=0.85,保持 active)
|
|
303
|
+
it("context 使用率 > 85% → 保持 active + 注入 wrap-up 指令", async () => {
|
|
304
|
+
const { pi } = makeFakePi();
|
|
305
|
+
const { ctx } = makeFakeCtx({ contextUsage: { tokens: 9000, contextWindow: 10000 } }); // 90% > 85%
|
|
306
|
+
const session = createGoalSession();
|
|
307
|
+
session.state = makeRunningState();
|
|
308
|
+
|
|
309
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
310
|
+
|
|
311
|
+
expect(result).toBeDefined();
|
|
312
|
+
expect(result!.message.customType).toBe("goal-context-exceeded");
|
|
313
|
+
// ADR-002:goal 保持 active(不转 paused)
|
|
314
|
+
expect(session.state!.status).toBe("active");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("context 使用率 <= 85% → 正常 context injection", async () => {
|
|
318
|
+
const { pi } = makeFakePi();
|
|
319
|
+
const { ctx } = makeFakeCtx({ contextUsage: { tokens: 5000, contextWindow: 10000 } }); // 50%
|
|
320
|
+
const session = createGoalSession();
|
|
321
|
+
session.state = makeRunningState();
|
|
322
|
+
|
|
323
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
324
|
+
|
|
325
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
326
|
+
expect(session.state!.status).toBe("active");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("非 active(blocked)→ 返回 undefined(不注入)", async () => {
|
|
330
|
+
const { pi } = makeFakePi();
|
|
331
|
+
const { ctx } = makeFakeCtx();
|
|
332
|
+
const session = createGoalSession();
|
|
333
|
+
session.state = makeRunningState({ status: "blocked" });
|
|
334
|
+
|
|
335
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
336
|
+
|
|
337
|
+
expect(result).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ── pending-notifications(W2 跨扩展集成)───────────────────
|
|
341
|
+
|
|
342
|
+
it("有活跃 pending operations → 注入提示含 pending 数量", async () => {
|
|
343
|
+
const { pi } = makeFakePi();
|
|
344
|
+
// 创建带 pending:register entries 的 ctx
|
|
345
|
+
const entries = [
|
|
346
|
+
{ customType: "pending:register", data: { id: "wf-1", type: "workflow", name: "test-wf" } },
|
|
347
|
+
{ customType: "pending:register", data: { id: "bg-1", type: "subagent", name: "worker" } },
|
|
348
|
+
];
|
|
349
|
+
const ctx = {
|
|
350
|
+
hasUI: true,
|
|
351
|
+
signal: { aborted: false } as AbortSignal,
|
|
352
|
+
getContextUsage: () => undefined,
|
|
353
|
+
ui: {
|
|
354
|
+
notify: vi.fn(),
|
|
355
|
+
setStatus: vi.fn(),
|
|
356
|
+
setWidget: vi.fn(),
|
|
357
|
+
theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
|
|
358
|
+
},
|
|
359
|
+
sessionManager: { getEntries: () => entries, getBranch: () => undefined },
|
|
360
|
+
} as unknown as ExtensionContext;
|
|
361
|
+
const session = createGoalSession();
|
|
362
|
+
session.state = makeRunningState();
|
|
363
|
+
|
|
364
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
365
|
+
|
|
366
|
+
expect(result).toBeDefined();
|
|
367
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
368
|
+
expect(result!.message.content).toContain("2 pending async operation(s)");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("有 pending register 但已 unregister → 不注入提示", async () => {
|
|
372
|
+
const { pi } = makeFakePi();
|
|
373
|
+
const entries = [
|
|
374
|
+
{ customType: "pending:register", data: { id: "wf-1", type: "workflow", name: "test-wf" } },
|
|
375
|
+
{ customType: "pending:unregister", data: { id: "wf-1" } },
|
|
376
|
+
];
|
|
377
|
+
const ctx = {
|
|
378
|
+
hasUI: true,
|
|
379
|
+
signal: { aborted: false } as AbortSignal,
|
|
380
|
+
getContextUsage: () => undefined,
|
|
381
|
+
ui: {
|
|
382
|
+
notify: vi.fn(),
|
|
383
|
+
setStatus: vi.fn(),
|
|
384
|
+
setWidget: vi.fn(),
|
|
385
|
+
theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
|
|
386
|
+
},
|
|
387
|
+
sessionManager: { getEntries: () => entries, getBranch: () => undefined },
|
|
388
|
+
} as unknown as ExtensionContext;
|
|
389
|
+
const session = createGoalSession();
|
|
390
|
+
session.state = makeRunningState();
|
|
391
|
+
|
|
392
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
393
|
+
|
|
394
|
+
expect(result).toBeDefined();
|
|
395
|
+
expect(result!.message.customType).toBe("goal-context");
|
|
396
|
+
expect(result!.message.content).not.toContain("pending async operation");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("无 pending entries → 正常注入(不含 pending 提示)", async () => {
|
|
400
|
+
const { pi } = makeFakePi();
|
|
401
|
+
const { ctx } = makeFakeCtx(); // 默认 getEntries: () => []
|
|
402
|
+
const session = createGoalSession();
|
|
403
|
+
session.state = makeRunningState();
|
|
404
|
+
|
|
405
|
+
const result = await handleBeforeAgentStart(pi, session, ctx);
|
|
406
|
+
|
|
407
|
+
expect(result).toBeDefined();
|
|
408
|
+
expect(result!.message.content).not.toContain("pending async operation");
|
|
409
|
+
});
|
|
410
|
+
});
|