@zhushanwen/pi-plan 0.5.2 → 1.1.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 +9 -9
- package/package.json +13 -11
- package/skills/plan-ext-config/SKILL.md +33 -18
- package/src/__tests__/active-path.test.ts +204 -0
- package/src/__tests__/command.test.ts +102 -32
- package/src/__tests__/enter.test.ts +58 -20
- package/src/__tests__/exec-options-alignment.test.ts +56 -0
- package/src/__tests__/execution-notice.test.ts +197 -0
- package/src/__tests__/{compact.test.ts → extract-plan-steps.test.ts} +24 -15
- package/src/__tests__/fsm-consumption.test.ts +388 -0
- package/src/__tests__/index.test.ts +175 -75
- package/src/__tests__/prompts.test.ts +66 -0
- package/src/__tests__/review-contract-boundary.test.ts +123 -0
- package/src/__tests__/review.test.ts +264 -97
- package/src/__tests__/skills.test.ts +14 -5
- package/src/__tests__/state.test.ts +254 -52
- package/src/__tests__/{compact-criteria-array.test.ts → success-criteria-array.test.ts} +29 -70
- package/src/__tests__/templates.test.ts +8 -25
- package/src/__tests__/tool.test.ts +269 -121
- package/src/command.ts +42 -34
- package/src/enter.ts +31 -50
- package/src/{compact.ts → execution-notice.ts} +30 -118
- package/src/index.ts +100 -88
- package/src/prompts.ts +42 -12
- package/src/state.ts +290 -87
- package/src/templates.ts +0 -15
- package/src/tool.ts +665 -257
- package/src/__tests__/compact-handler.test.ts +0 -348
- package/src/__tests__/exec-skills.test.ts +0 -397
- package/src/exec-skills.ts +0 -407
|
@@ -1,31 +1,25 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
// index.ts 只做装配:mock 掉子模块注册函数,只捕获 pi.on 的 hook 与
|
|
3
|
+
// index.ts 只做装配:mock 掉子模块注册函数,只捕获 pi.on 的 hook 与 planCtx 注册表
|
|
4
4
|
vi.mock("../tool.js", () => ({
|
|
5
5
|
registerPlanTool: vi.fn(
|
|
6
|
-
(_pi: unknown,
|
|
7
|
-
captured.
|
|
6
|
+
(_pi: unknown, planCtx: { controllers: Map<string, AbortController> }) => {
|
|
7
|
+
captured.planCtx = planCtx;
|
|
8
8
|
},
|
|
9
9
|
),
|
|
10
|
-
PLAN_MODE_TOOLS: ["read", "bash", "grep", "find", "ls", "plan"],
|
|
10
|
+
PLAN_MODE_TOOLS: ["read", "bash", "grep", "find", "ls", "plan", "ask_user"],
|
|
11
|
+
// E3 宿主分流(F-W3-1)依赖:与 tool.ts 实装同源的一行 env 读(受 vi.stubEnv 控制)
|
|
12
|
+
isTaijiHost: () => process.env.TAIJI_AGENT_EXT_LOG === "1",
|
|
11
13
|
}));
|
|
12
14
|
vi.mock("../command.js", () => ({ registerPlanCommand: vi.fn() }));
|
|
13
|
-
vi.mock("../compact.js", () => ({ registerPlanEventHandlers: vi.fn() }));
|
|
14
15
|
vi.mock("../widget.js", () => ({ updatePlanWidget: vi.fn() }));
|
|
15
16
|
|
|
16
|
-
// MF-1-8:注入失败日志必须走 extension-logger(stderr 仅 logger 自身抛错的内层兜底)——
|
|
17
|
-
// 捕获 warn spy 断言落盘通道
|
|
18
|
-
const { loggerWarn } = vi.hoisted(() => ({ loggerWarn: vi.fn() }));
|
|
19
|
-
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
20
|
-
getLogger: () => ({ warn: loggerWarn, error: vi.fn() }),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
18
|
|
|
25
19
|
import planExtension from "../index.js";
|
|
26
20
|
import { PLAN_CONTEXT_CUSTOM_TYPE } from "../state.js";
|
|
27
21
|
|
|
28
|
-
const captured: {
|
|
22
|
+
const captured: { planCtx?: { controllers: Map<string, AbortController> } } = {};
|
|
29
23
|
|
|
30
24
|
type Handler = (event: unknown, ctx: ExtensionContext) => Promise<unknown> | unknown;
|
|
31
25
|
|
|
@@ -44,10 +38,12 @@ function setup() {
|
|
|
44
38
|
return { handlers, pi };
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
function makeCtx(entries: unknown[]): ExtensionContext {
|
|
41
|
+
function makeCtx(entries: unknown[], mode?: string): ExtensionContext {
|
|
48
42
|
return {
|
|
43
|
+
mode,
|
|
49
44
|
sessionManager: {
|
|
50
45
|
getSessionId: () => "test-session",
|
|
46
|
+
getLeafId: () => null,
|
|
51
47
|
getEntries: () => entries,
|
|
52
48
|
},
|
|
53
49
|
ui: { setWidget: vi.fn(), setStatus: vi.fn(), theme: { fg: (_t: string, text: string) => text } },
|
|
@@ -60,7 +56,7 @@ function planStateEntry(data: Record<string, unknown>) {
|
|
|
60
56
|
|
|
61
57
|
beforeEach(() => {
|
|
62
58
|
vi.clearAllMocks();
|
|
63
|
-
captured.
|
|
59
|
+
captured.planCtx = undefined;
|
|
64
60
|
vi.stubEnv("TAIJI_AGENT_EXT_LOG", "");
|
|
65
61
|
});
|
|
66
62
|
|
|
@@ -68,8 +64,8 @@ afterEach(() => {
|
|
|
68
64
|
vi.unstubAllEnvs();
|
|
69
65
|
});
|
|
70
66
|
|
|
71
|
-
describe("session_start hook(E3
|
|
72
|
-
it("
|
|
67
|
+
describe("session_start hook(E3:按 state 查表恢复)", () => {
|
|
68
|
+
it("reviewing + no live select → steers the agent to re-call submit-review with 上轮 selfReview 全文(D9④)", async () => {
|
|
73
69
|
const { handlers, pi } = setup();
|
|
74
70
|
const ctx = makeCtx([
|
|
75
71
|
planStateEntry({
|
|
@@ -79,13 +75,14 @@ describe("session_start hook(E3:awaiting 重挂提醒)", () => {
|
|
|
79
75
|
templateName: "",
|
|
80
76
|
skills: ["tech-design"],
|
|
81
77
|
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "tech-design", version: 1 }],
|
|
82
|
-
|
|
78
|
+
state: "reviewing",
|
|
79
|
+
selfReview: "3 requirements covered; 2 assumptions verified.",
|
|
83
80
|
}),
|
|
84
81
|
]);
|
|
85
82
|
|
|
86
83
|
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
87
84
|
|
|
88
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
85
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan", "ask_user"]);
|
|
89
86
|
// custom message 三要素 + streaming steer options 断言(A6)
|
|
90
87
|
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
91
88
|
{
|
|
@@ -95,11 +92,104 @@ describe("session_start hook(E3:awaiting 重挂提醒)", () => {
|
|
|
95
92
|
},
|
|
96
93
|
{ deliverAs: "steer", triggerTurn: true },
|
|
97
94
|
);
|
|
98
|
-
//
|
|
99
|
-
|
|
95
|
+
// D9④:steer 携带上轮 selfReview 全文 + 原样回传指令(豁免只在「自审内容」,过门义务不豁免)
|
|
96
|
+
const steerContent = (pi.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0].content as string;
|
|
97
|
+
expect(steerContent).toContain("3 requirements covered; 2 assumptions verified.");
|
|
98
|
+
expect(steerContent).toContain("VERBATIM");
|
|
99
|
+
expect(steerContent).toContain("selfReview");
|
|
100
|
+
// E3 重挂即落盘(§3.4 降级两源):resumeHint='resubmit'(旧 reviewStateSource 语义取代)
|
|
101
|
+
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
102
|
+
"plan-state",
|
|
103
|
+
expect.objectContaining({ isActive: true, state: "reviewing", resumeHint: "resubmit" }),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("旧 entry(reviewState='awaiting' 无 state 字段)经映射同样落 reviewing 重挂分支(D2 读方①)", async () => {
|
|
108
|
+
const { handlers, pi } = setup();
|
|
109
|
+
const ctx = makeCtx([
|
|
110
|
+
planStateEntry({
|
|
111
|
+
isActive: true,
|
|
112
|
+
planFilePath: "/p/plan.md",
|
|
113
|
+
requirement: "r",
|
|
114
|
+
templateName: "",
|
|
115
|
+
skills: [],
|
|
116
|
+
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "", version: 1 }],
|
|
117
|
+
reviewState: "awaiting",
|
|
118
|
+
}),
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
122
|
+
|
|
123
|
+
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({ content: expect.stringContaining("submit-review") }),
|
|
125
|
+
expect.anything(),
|
|
126
|
+
);
|
|
127
|
+
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
128
|
+
"plan-state",
|
|
129
|
+
expect.objectContaining({ state: "reviewing", resumeHint: "resubmit" }),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("taiji GUI 宿主(signal=1 + mode=rpc)reviewing → 不自动 steer 重挂(F-W3-1 审批条复活拦截),只落 resumeHint 交 degraded 按钮恢复", async () => {
|
|
134
|
+
vi.stubEnv("TAIJI_AGENT_EXT_LOG", "1");
|
|
135
|
+
const { handlers, pi } = setup();
|
|
136
|
+
const ctx = makeCtx(
|
|
137
|
+
[
|
|
138
|
+
planStateEntry({
|
|
139
|
+
isActive: true,
|
|
140
|
+
planFilePath: "/p/plan.md",
|
|
141
|
+
requirement: "r",
|
|
142
|
+
templateName: "",
|
|
143
|
+
skills: [],
|
|
144
|
+
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "", version: 1 }],
|
|
145
|
+
state: "reviewing",
|
|
146
|
+
selfReview: "carried conclusions",
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
"rpc",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
153
|
+
|
|
154
|
+
// 审批条不自动复活:E3 不得无人值守重提审批(steer triggerTurn = 自动重提通道)
|
|
155
|
+
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
156
|
+
// 工具集收拢照常(E3 的非交互恢复职责保留)
|
|
157
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan", "ask_user"]);
|
|
158
|
+
// resumeHint 落盘:renderer degraded 分支(reviewing ∧ 无挂起)据此呈现
|
|
159
|
+
// 「审批提问已随会话重启失效 + 重新提交审批」按钮——恢复触发权归用户
|
|
160
|
+
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
161
|
+
"plan-state",
|
|
162
|
+
expect.objectContaining({ isActive: true, state: "reviewing", resumeHint: "resubmit" }),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("taiji 宿主 env 泄漏到非 rpc 形态(signal=1 + mode=tui)→ steer 照旧(与 executeSubmitReview 的 E8 分流对齐)", async () => {
|
|
167
|
+
vi.stubEnv("TAIJI_AGENT_EXT_LOG", "1");
|
|
168
|
+
const { handlers, pi } = setup();
|
|
169
|
+
const ctx = makeCtx(
|
|
170
|
+
[
|
|
171
|
+
planStateEntry({
|
|
172
|
+
isActive: true,
|
|
173
|
+
planFilePath: "/p/plan.md",
|
|
174
|
+
requirement: "r",
|
|
175
|
+
templateName: "",
|
|
176
|
+
skills: [],
|
|
177
|
+
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "", version: 1 }],
|
|
178
|
+
state: "reviewing",
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
"tui",
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
185
|
+
|
|
186
|
+
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
187
|
+
expect.objectContaining({ content: expect.stringContaining("submit-review") }),
|
|
188
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
189
|
+
);
|
|
100
190
|
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
101
191
|
"plan-state",
|
|
102
|
-
expect.objectContaining({
|
|
192
|
+
expect.objectContaining({ state: "reviewing", resumeHint: "resubmit" }),
|
|
103
193
|
);
|
|
104
194
|
});
|
|
105
195
|
|
|
@@ -113,13 +203,13 @@ describe("session_start hook(E3:awaiting 重挂提醒)", () => {
|
|
|
113
203
|
templateName: "",
|
|
114
204
|
skills: ["tech-design"],
|
|
115
205
|
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "tech-design", version: 1 }],
|
|
116
|
-
|
|
206
|
+
state: "revising",
|
|
117
207
|
}),
|
|
118
208
|
]);
|
|
119
209
|
|
|
120
210
|
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
121
211
|
|
|
122
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
212
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan", "ask_user"]);
|
|
123
213
|
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
124
214
|
{
|
|
125
215
|
customType: PLAN_CONTEXT_CUSTOM_TYPE,
|
|
@@ -128,24 +218,56 @@ describe("session_start hook(E3:awaiting 重挂提醒)", () => {
|
|
|
128
218
|
},
|
|
129
219
|
{ deliverAs: "steer", triggerTurn: true },
|
|
130
220
|
);
|
|
131
|
-
// revising 恢复不落
|
|
221
|
+
// revising 恢复不落 resumeHint('resubmit' 只描述 reviewing 降级等待;revising 由
|
|
132
222
|
// steer triggerTurn 立即开轮接续,恢复期间显示的 revising 是真实进行中)
|
|
133
223
|
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
134
224
|
});
|
|
135
225
|
|
|
226
|
+
it("dispatching + no live select → review_aborted 边落 approved + steer 重调 complete(D5 E3 新支)", async () => {
|
|
227
|
+
const { handlers, pi } = setup();
|
|
228
|
+
const ctx = makeCtx([
|
|
229
|
+
planStateEntry({
|
|
230
|
+
isActive: true,
|
|
231
|
+
planFilePath: "/p/plan.md",
|
|
232
|
+
requirement: "r",
|
|
233
|
+
templateName: "",
|
|
234
|
+
skills: [],
|
|
235
|
+
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "", version: 1 }],
|
|
236
|
+
state: "dispatching",
|
|
237
|
+
}),
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
241
|
+
|
|
242
|
+
// 崩溃消散的表单 = 无选择解散极端形态:dispatching --review_aborted--> approved 落盘
|
|
243
|
+
//(批准事实保留,且使重调 complete 的 approve 边合法)
|
|
244
|
+
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
245
|
+
"plan-state",
|
|
246
|
+
expect.objectContaining({ state: "approved", isActive: true }),
|
|
247
|
+
);
|
|
248
|
+
expect(pi.sendMessage).toHaveBeenCalledWith(
|
|
249
|
+
{
|
|
250
|
+
customType: PLAN_CONTEXT_CUSTOM_TYPE,
|
|
251
|
+
content: expect.stringContaining("plan(action='complete')"),
|
|
252
|
+
display: false,
|
|
253
|
+
},
|
|
254
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
136
258
|
it("stale controllers are cleared on session rebuild (禁复用已 abort 的 controller)", async () => {
|
|
137
259
|
const { handlers } = setup();
|
|
138
260
|
const ctx = makeCtx([]);
|
|
139
261
|
// 同进程 reload 场景:注册表里残留旧 controller——hook 重建 session 时必须清掉
|
|
140
262
|
// (残留的已 abort controller 禁止复用:发起新挂起 select 会 fresh 新建,此处是防御清理)
|
|
141
|
-
captured.controllers
|
|
263
|
+
captured.planCtx!.controllers.set("test-session", new AbortController());
|
|
142
264
|
|
|
143
265
|
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
144
266
|
|
|
145
|
-
expect(captured.controllers
|
|
267
|
+
expect(captured.planCtx!.controllers.has("test-session")).toBe(false);
|
|
146
268
|
});
|
|
147
269
|
|
|
148
|
-
it("active without
|
|
270
|
+
it("active without pending review (in progress) → no reminder", async () => {
|
|
149
271
|
const { handlers, pi } = setup();
|
|
150
272
|
const ctx = makeCtx([
|
|
151
273
|
planStateEntry({
|
|
@@ -162,67 +284,45 @@ describe("session_start hook(E3:awaiting 重挂提醒)", () => {
|
|
|
162
284
|
|
|
163
285
|
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
164
286
|
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
165
|
-
// 非
|
|
287
|
+
// 非 reviewing/dispatching 不落盘:E3 resumeHint 只在 reviewing 重挂分支写
|
|
166
288
|
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
167
289
|
});
|
|
168
290
|
|
|
169
|
-
it("
|
|
291
|
+
it("approved(later 后重开)不打扰:无 E3 重挂(S16「重开 session 无 E3 重挂打扰」)", async () => {
|
|
170
292
|
const { handlers, pi } = setup();
|
|
171
|
-
const ctx = makeCtx([
|
|
293
|
+
const ctx = makeCtx([
|
|
294
|
+
planStateEntry({
|
|
295
|
+
isActive: true,
|
|
296
|
+
planFilePath: "/p/plan.md",
|
|
297
|
+
requirement: "r",
|
|
298
|
+
templateName: "",
|
|
299
|
+
skills: [],
|
|
300
|
+
docs: [{ fileName: "design.md", absPath: "/p/design.md", sourceSkill: "", version: 1 }],
|
|
301
|
+
state: "approved",
|
|
302
|
+
}),
|
|
303
|
+
]);
|
|
172
304
|
|
|
173
305
|
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
174
306
|
|
|
175
|
-
expect(pi.setActiveTools).
|
|
307
|
+
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
176
308
|
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
309
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
177
310
|
});
|
|
178
|
-
});
|
|
179
311
|
|
|
180
|
-
|
|
181
|
-
|
|
312
|
+
it("inactive plan → no reminder, no tool restriction", async () => {
|
|
313
|
+
const { handlers, pi } = setup();
|
|
314
|
+
const ctx = makeCtx([]);
|
|
182
315
|
|
|
183
|
-
|
|
184
|
-
const { handlers } = setup();
|
|
185
|
-
const result = handlers.get("before_agent_start")!(
|
|
186
|
-
{ type: "before_agent_start", prompt: "hi", systemPrompt: basePrompt },
|
|
187
|
-
makeCtx([]),
|
|
188
|
-
);
|
|
189
|
-
expect(result).toBeUndefined();
|
|
190
|
-
});
|
|
316
|
+
await handlers.get("session_start")!({ type: "session_start" }, ctx);
|
|
191
317
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const { handlers } = setup();
|
|
195
|
-
const result = handlers.get("before_agent_start")!(
|
|
196
|
-
{ type: "before_agent_start", prompt: "hi", systemPrompt: basePrompt },
|
|
197
|
-
makeCtx([]),
|
|
198
|
-
) as { systemPrompt?: string } | undefined;
|
|
199
|
-
|
|
200
|
-
expect(result?.systemPrompt).toContain(basePrompt);
|
|
201
|
-
// agent 自助进入引导在场(enter action + 无需确认)
|
|
202
|
-
expect(result?.systemPrompt).toContain("plan(action='enter'");
|
|
203
|
-
expect(result?.systemPrompt).toContain("Do not ask for permission to enter");
|
|
204
|
-
// 旧的「建议 + 需确认」措辞已移除(enter 是 tool action,无需确认闸门)
|
|
205
|
-
expect(result?.systemPrompt).not.toContain("Do NOT enter plan mode without the user's confirmation");
|
|
318
|
+
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
319
|
+
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
206
320
|
});
|
|
321
|
+
});
|
|
207
322
|
|
|
208
|
-
|
|
209
|
-
|
|
323
|
+
describe("进入引导单源(systemPrompt 注入面已收敛到 tool promptSnippet)", () => {
|
|
324
|
+
it("before_agent_start 钩子不注册:taiji 宿主注入面与 tool 描述面不再同时含进入引导(C1 行为变更组锚)", () => {
|
|
210
325
|
const { handlers } = setup();
|
|
211
|
-
|
|
212
|
-
type: "before_agent_start",
|
|
213
|
-
get systemPrompt(): string {
|
|
214
|
-
throw new Error("boom");
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const result = handlers.get("before_agent_start")!(evilEvent, makeCtx([]));
|
|
219
|
-
|
|
220
|
-
// Never block the agent loop:吞错返回 undefined
|
|
221
|
-
expect(result).toBeUndefined();
|
|
222
|
-
// 日志走 extension-logger 文件通道(~/.pi/agent/logs/),不再 stderr 直写
|
|
223
|
-
expect(loggerWarn).toHaveBeenCalledWith(
|
|
224
|
-
"plan: before_agent_start injection failed",
|
|
225
|
-
expect.objectContaining({ error: expect.stringContaining("boom") }),
|
|
226
|
-
);
|
|
326
|
+
expect(handlers.has("before_agent_start")).toBe(false);
|
|
227
327
|
});
|
|
228
328
|
});
|
|
@@ -72,3 +72,69 @@ describe("注入段项目级模板源锚点 = PlanPromptInput.projectRoot(ctx.
|
|
|
72
72
|
expect(prompt).not.toContain(`<location>${join(trapDir, `${trapName}.md`)}</location>`);
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("Phase D 段注入 buildPlanModePrompt(对齐测试的装配面锚)", () => {
|
|
77
|
+
it("Phase D 段的两个注入分支(模板流程 / --template 直传)都携带该段(若装配漏注入,exec-options-alignment 的散文锚测不到)", () => {
|
|
78
|
+
const projectRoot = mkTmpDir("plan-prompt-phased-");
|
|
79
|
+
const base = {
|
|
80
|
+
requirement: "add dark mode",
|
|
81
|
+
planFilePath: join(projectRoot, ".tmp", "plans", "add-dark-mode", "plan.md"),
|
|
82
|
+
projectRoot,
|
|
83
|
+
skills: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// 分支一:无 --skills 的模板流程(prompts.ts else-if 注入腿)
|
|
87
|
+
const templateFlow = buildPlanModePrompt(base);
|
|
88
|
+
expect(templateFlow).toContain("Phase D: Completion");
|
|
89
|
+
expect(templateFlow).toContain("picks an execution method in the completion dialog");
|
|
90
|
+
|
|
91
|
+
// 分支二:--template 直传变体(prompts.ts if 注入腿;技能流程分支无 Phase D 属设计现状,不锚定)
|
|
92
|
+
const directFlow = buildPlanModePrompt({
|
|
93
|
+
...base,
|
|
94
|
+
template: { absPath: "/tmp/plans-templates/feature.md", content: "# Feature\n## Implementation Steps\n1. Do it" },
|
|
95
|
+
});
|
|
96
|
+
expect(directFlow).toContain("Phase D: Completion");
|
|
97
|
+
expect(directFlow).toContain("Template (via --template)");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("Phase C.5 自审清单 + D8 重挂请求纪律 + selfReview 必带(D9②/D8)", () => {
|
|
102
|
+
it("模板流程:C.5 四项清单在产物纪律段之后注入,submit-review 指引必带 selfReview,重挂请求直接重提", () => {
|
|
103
|
+
const projectRoot = mkTmpDir("plan-prompt-c5-");
|
|
104
|
+
const prompt = buildPlanModePrompt({
|
|
105
|
+
requirement: "add dark mode",
|
|
106
|
+
planFilePath: join(projectRoot, ".tmp", "plans", "add-dark-mode", "plan.md"),
|
|
107
|
+
projectRoot,
|
|
108
|
+
skills: [],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Phase C.5 自审清单(D9②):四项 + 先修文档再提交
|
|
112
|
+
expect(prompt).toContain("Phase C.5: Self-Review Before Submission");
|
|
113
|
+
expect(prompt).toContain("Requirement coverage");
|
|
114
|
+
expect(prompt).toContain("Assumption audit");
|
|
115
|
+
expect(prompt).toContain("[UNVERIFIED]");
|
|
116
|
+
expect(prompt).toContain("Chapter completeness");
|
|
117
|
+
expect(prompt).toContain("Acceptance realism");
|
|
118
|
+
// C.5 段落在产物纪律段之后(D9② 注入位钉死)
|
|
119
|
+
expect(prompt.indexOf("## Deliverable Discipline")).toBeLessThan(prompt.indexOf("Phase C.5"));
|
|
120
|
+
// 自审硬门的提示词面:submit-review 必带 selfReview(无豁免)
|
|
121
|
+
expect(prompt).toContain("selfReview is REQUIRED every time");
|
|
122
|
+
expect(prompt).toContain("plan(action='submit-review', selfReview=");
|
|
123
|
+
// D8 提示词纪律:收到重挂请求直接 submit-review(不再让用户猜/让 agent 无指令)
|
|
124
|
+
expect(prompt).toContain("asks to re-submit the plan review");
|
|
125
|
+
expect(prompt).toContain("IMMEDIATELY");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("技能流程同样注入 C.5(自审硬门对每次 submit-review 生效,不随模板/技能流程分叉)", () => {
|
|
129
|
+
const projectRoot = mkTmpDir("plan-prompt-c5s-");
|
|
130
|
+
const prompt = buildPlanModePrompt({
|
|
131
|
+
requirement: "refactor auth",
|
|
132
|
+
planFilePath: join(projectRoot, ".tmp", "plans", "refactor-auth", "plan.md"),
|
|
133
|
+
projectRoot,
|
|
134
|
+
skills: [{ name: "tech-design", skillPath: "/skills/tech-design/SKILL.md" }],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(prompt).toContain("Phase C.5: Self-Review Before Submission");
|
|
138
|
+
expect(prompt).toContain("selfReview is REQUIRED every time");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock typebox / pi-ai before importing tool(形态照 review.test.ts)
|
|
4
|
+
vi.mock("typebox", () => ({
|
|
5
|
+
Type: {
|
|
6
|
+
Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
|
|
7
|
+
String: (opts?: Record<string, unknown>) => ({ type: "string", ...opts }),
|
|
8
|
+
Optional: (schema: unknown) => schema,
|
|
9
|
+
Array: (item: unknown, opts?: Record<string, unknown>) => ({ type: "array", items: item, ...opts }),
|
|
10
|
+
},
|
|
11
|
+
Static: class {},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("@earendil-works/pi-ai", () => ({
|
|
15
|
+
StringEnum: (values: readonly string[]) => ({ type: "string", enum: [...values] }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("../execution-notice.js", async () => {
|
|
19
|
+
const { GOAL_FAILURE_RECOVERY } = await vi.importActual<typeof import("../execution-notice.js")>("../execution-notice.js");
|
|
20
|
+
return {
|
|
21
|
+
handlePlanComplete: vi.fn(),
|
|
22
|
+
GOAL_FAILURE_RECOVERY,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock("../widget.js", () => ({ updatePlanWidget: vi.fn() }));
|
|
27
|
+
// importActual 展开:只覆写 detectExecSkills(不扫真实目录),其余导出(含 enter.ts
|
|
28
|
+
// re-export 的 resolveSkills 执行门禁)走真实现——mock 罩全模块会把它一并变 undefined
|
|
29
|
+
vi.mock("@zhushanwen/pi-exec-skills", async () => {
|
|
30
|
+
const actual = await vi.importActual<typeof import("@zhushanwen/pi-exec-skills")>(
|
|
31
|
+
"@zhushanwen/pi-exec-skills",
|
|
32
|
+
);
|
|
33
|
+
return { ...actual, detectExecSkills: vi.fn(() => []) };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
isPlanReviewRequest,
|
|
38
|
+
PLAN_REVIEW_MARKER,
|
|
39
|
+
PLAN_SELF_REVIEW_MAX_BYTES,
|
|
40
|
+
} from "@zhushanwen/extension-protocol";
|
|
41
|
+
|
|
42
|
+
import type { PlanState } from "../state.js";
|
|
43
|
+
import { createPlanCtx, DEFAULT_PLAN_STATE } from "../state.js";
|
|
44
|
+
import { registerPlanTool } from "../tool.js";
|
|
45
|
+
|
|
46
|
+
const ALL_TOOL_NAMES = ["read", "bash", "grep", "find", "ls", "plan", "ask_user", "write", "edit"];
|
|
47
|
+
|
|
48
|
+
function planningState(): PlanState {
|
|
49
|
+
return {
|
|
50
|
+
...DEFAULT_PLAN_STATE,
|
|
51
|
+
isActive: true,
|
|
52
|
+
planFilePath: "/tmp/test-project/.tmp/plans/auth/plan.md",
|
|
53
|
+
requirement: "refactor auth",
|
|
54
|
+
docs: [{ fileName: "design.md", absPath: "/tmp/test-project/.tmp/plans/auth/design.md", sourceSkill: "", version: 1 }],
|
|
55
|
+
state: "planning",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setup() {
|
|
60
|
+
const planCtx = createPlanCtx();
|
|
61
|
+
let executeFn: (id: string, p: Record<string, unknown>, sig?: AbortSignal, upd?: unknown, ctx?: unknown) => Promise<unknown>;
|
|
62
|
+
const pi = {
|
|
63
|
+
registerTool: vi.fn((tool) => { executeFn = tool.execute; }),
|
|
64
|
+
appendEntry: vi.fn(),
|
|
65
|
+
setActiveTools: vi.fn(),
|
|
66
|
+
sendMessage: vi.fn(),
|
|
67
|
+
getCommands: vi.fn(() => []),
|
|
68
|
+
getAllTools: vi.fn(() => ALL_TOOL_NAMES.map((n) => ({ name: n }))),
|
|
69
|
+
} as unknown as Parameters<typeof registerPlanTool>[0];
|
|
70
|
+
registerPlanTool(pi, planCtx);
|
|
71
|
+
|
|
72
|
+
const ctx = {
|
|
73
|
+
sessionId: "test-session",
|
|
74
|
+
cwd: "/tmp/test-project",
|
|
75
|
+
hasUI: true,
|
|
76
|
+
mode: "rpc" as const,
|
|
77
|
+
isProjectTrusted: () => true,
|
|
78
|
+
sessionManager: { getSessionId: () => "test-session", getEntries: () => [] },
|
|
79
|
+
ui: { select: vi.fn(), notify: vi.fn() },
|
|
80
|
+
};
|
|
81
|
+
planCtx.states.set("test-session", planningState());
|
|
82
|
+
|
|
83
|
+
const exec = (params: Record<string, unknown>) => executeFn!("tc0", params, undefined, undefined, ctx);
|
|
84
|
+
return { pi, ctx, exec };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 取 marker payload(submit-review select options[0] 解析结果) */
|
|
88
|
+
function markerPayload(ctx: { ui: { select: unknown } }): unknown {
|
|
89
|
+
const calls = (ctx.ui.select as ReturnType<typeof vi.fn>).mock.calls;
|
|
90
|
+
const markerCall = calls.find((c) => c[0] === PLAN_REVIEW_MARKER);
|
|
91
|
+
return JSON.parse((markerCall![1] as string[])[0]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
vi.stubEnv("TAIJI_AGENT_EXT_LOG", "1");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
vi.unstubAllEnvs();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── 契约纯函数(error envelope / boundary 帧 / truncateSelfReview)的 canonical 测试
|
|
103
|
+
// 在 packages/extension-protocol/src/extensions/plan/review-contract.test.ts(更强版本:
|
|
104
|
+
// 键剥除结构断言 / 4KB 字节界精确锚 / 刁钻样本超集)——本文件只保留经 tool 消费面的
|
|
105
|
+
// 接线契约,不重复测共享逻辑。──
|
|
106
|
+
|
|
107
|
+
// ── 经 tool 消费面的契约落地(consumers.md ①③:payload 构造单点 + 双分源降级)──
|
|
108
|
+
|
|
109
|
+
describe("契约经 executeSubmitReview 消费面落地", () => {
|
|
110
|
+
it("超限 selfReview:payload 构造单点截断(写侧 4KB),且产出 payload 过入站守卫(boundary 帧合法)", async () => {
|
|
111
|
+
const { ctx, exec } = setup();
|
|
112
|
+
const oversized = "自审".repeat(PLAN_SELF_REVIEW_MAX_BYTES); // 远超 4KB
|
|
113
|
+
(ctx.ui.select as ReturnType<typeof vi.fn>).mockResolvedValue(JSON.stringify({ decision: "dismiss" }));
|
|
114
|
+
|
|
115
|
+
await exec({ action: "submit-review", selfReview: oversized });
|
|
116
|
+
|
|
117
|
+
const payload = markerPayload(ctx) as { selfReview: string };
|
|
118
|
+
expect(Buffer.byteLength(payload.selfReview, "utf8")).toBeLessThanOrEqual(PLAN_SELF_REVIEW_MAX_BYTES);
|
|
119
|
+
// 截断后仍是合法 boundary 帧(消费侧守卫必过)
|
|
120
|
+
expect(isPlanReviewRequest(payload)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
});
|