@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * plan/compact.ts — buildPlanSuccessCriteria 数组形态测试
2
+ * plan/execution-notice.ts — buildPlanSuccessCriteria 数组形态测试
3
3
  *
4
4
  * 形态契约(U25):1 条总述 `All N steps of <basename> executed and verified`
5
5
  * + 前 3 条 step preview(编号前缀、单条截断 ≤80 chars),合计 ≤4 条
@@ -11,7 +11,7 @@ vi.mock("node:fs", () => ({
11
11
  readFileSync: vi.fn(),
12
12
  }));
13
13
 
14
- import { buildPlanSuccessCriteria, handlePlanComplete } from "../compact.js";
14
+ import { buildPlanSuccessCriteria, handlePlanComplete } from "../execution-notice.js";
15
15
  import { PLAN_CONTEXT_CUSTOM_TYPE } from "../state.js";
16
16
 
17
17
  const fsMock = vi.mocked(await import("node:fs"));
@@ -25,25 +25,18 @@ function makePi() {
25
25
  }
26
26
 
27
27
  function makeCtx() {
28
- const onCompleteFns: Array<() => void> = [];
29
- const onErrorFns: Array<(e: Error) => void> = [];
30
-
31
28
  return {
32
- sessionManager: { getSessionId: () => "test-session", getEntries: () => [] as unknown[] },
29
+ sessionManager: { getSessionId: () => "test-session", getLeafId: () => null, getEntries: () => [] as unknown[] },
33
30
  ui: { notify: vi.fn() },
34
- compact: vi.fn((opts: { onComplete?: () => void; onError?: (e: Error) => void }) => {
35
- if (opts.onComplete) onCompleteFns.push(opts.onComplete);
36
- if (opts.onError) onErrorFns.push(opts.onError);
37
- }),
38
- _onCompleteFns: onCompleteFns,
39
- _onErrorFns: onErrorFns,
40
31
  };
41
32
  }
42
33
 
43
34
  function makeActiveState() {
44
35
  return {
45
36
  isActive: true,
46
- planFilePath: "/tmp/plan.md",
37
+ // 生产真实形态(enter.ts:<project>/.tmp/plans/<slug>/plan.md)——buildPlanSlug 取
38
+ // 目录名(requirement slug 段),合成单层路径会让 slug 派生在测试数据上失真
39
+ planFilePath: "/tmp/test-project/.tmp/plans/login-page/plan.md",
47
40
  requirement: "Add login page",
48
41
  templateName: "default",
49
42
  };
@@ -66,46 +59,37 @@ function expectSingleLineArray(value: unknown): string[] {
66
59
  // --- buildPlanSuccessCriteria 单元 ---
67
60
 
68
61
  describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
62
+ // 生产真实形态(enter.ts:<project>/.tmp/plans/<slug>/plan.md)——总述身份取目录名
63
+ const PLAN_PATH = "/tmp/test-project/.tmp/plans/login-page/plan.md";
64
+
69
65
  it("5 步 plan → 4 条:总述 + 前 3 条 preview", () => {
70
66
  const steps = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
71
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", steps));
67
+ const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, steps));
72
68
 
73
69
  expect(items).toHaveLength(4);
74
- expect(items[0]).toBe("All 5 steps of plan executed and verified");
70
+ expect(items[0]).toBe("All 5 steps of login-page executed and verified");
75
71
  expect(items.slice(1)).toEqual(["1. Alpha", "2. Bravo", "3. Charlie"]);
76
72
  });
77
73
 
78
- it("1 步 plan → 2 条", () => {
79
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", ["Only step"]));
80
-
81
- expect(items).toHaveLength(2);
82
- expect(items[0]).toBe("All 1 steps of plan executed and verified");
83
- expect(items[1]).toBe("1. Only step");
84
- });
85
-
86
74
  it("0 步 → 仅总述 1 条", () => {
87
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", []));
75
+ const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, []));
88
76
 
89
- expect(items).toEqual(["All 0 steps of plan executed and verified"]);
77
+ expect(items).toEqual(["All 0 steps of login-page executed and verified"]);
90
78
  });
91
79
 
92
- it("12 步 → 仍 4 条,总述含实际总数", () => {
93
- const steps = Array.from({ length: 12 }, (_, i) => `S${i + 1}`);
94
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", steps));
95
-
96
- expect(items).toHaveLength(4);
97
- expect(items[0]).toBe("All 12 steps of plan executed and verified");
98
- expect(items.slice(1)).toEqual(["1. S1", "2. S2", "3. S3"]);
80
+ it("目录名混合大小写 → kebab 链小写折叠(非生产形态防御)", () => {
81
+ const items = buildPlanSuccessCriteria("/work/FeatLogin/plan.md", ["S1"]);
82
+ expect(items[0]).toBe("All 1 steps of featlogin executed and verified");
99
83
  });
100
84
 
101
- it("basename 为 plan 文件名去扩展名(含大写 .MD)", () => {
102
- const items = buildPlanSuccessCriteria("/work/feat-login-plan.MD", ["S1"]);
103
- expect(items[0]).toBe("All 1 steps of feat-login-plan executed and verified");
85
+ it("非生产形态裸文件路径 → 提取为空回兜底名", () => {
86
+ const items = buildPlanSuccessCriteria("/plan-x.md", ["S1"]);
87
+ expect(items[0]).toBe("All 1 steps of plan-execution executed and verified");
104
88
  });
105
89
 
106
90
  it("超长 step → 截断至 ≤80 chars 且以 ... 结尾,保留编号前缀", () => {
107
91
  const long = "x".repeat(120);
108
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", [long]));
92
+ const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, [long]));
109
93
 
110
94
  expect(items[1]).toHaveLength(80);
111
95
  expect(items[1].endsWith("...")).toBe(true);
@@ -114,7 +98,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
114
98
 
115
99
  it("恰好 80 chars 的条目 → 不截断、不加省略号", () => {
116
100
  const exact = "y".repeat(77); // "1. " 前缀 + 77 = 80
117
- const items = buildPlanSuccessCriteria("/tmp/plan.md", [exact]);
101
+ const items = buildPlanSuccessCriteria(PLAN_PATH, [exact]);
118
102
 
119
103
  expect(items[1]).toBe(`1. ${exact}`);
120
104
  expect(items[1]).toHaveLength(80);
@@ -122,7 +106,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
122
106
  });
123
107
 
124
108
  it("step 文本含换行符 → 折叠为单行空格分隔(goal handler 拒 \r\n)", () => {
125
- const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", ["line1\nline2\r\nline3"]));
109
+ const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, ["line1\nline2\r\nline3"]));
126
110
 
127
111
  expect(items[1]).toBe("1. line1 line2 line3");
128
112
  });
@@ -131,7 +115,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
131
115
  // --- handlePlanComplete → tryGoalInit 端到端 ---
132
116
 
133
117
  /**
134
- * goal 桥 slot key——与 compact.ts / goal 侧 index.ts 的字符串一致(本地声明,
118
+ * goal 桥 slot key——与 execution-notice.ts / goal 侧 index.ts 的字符串一致(本地声明,
135
119
  * 不 import 对方包:pi-goal 是 optional peer)。mock 挂 slot 与真实通道同构。
136
120
  */
137
121
  const GOAL_INIT_SLOT_KEY = Symbol.for("@zhushanwen/pi-goal.goalInit");
@@ -158,51 +142,26 @@ describe("handlePlanComplete — goalInit slot 第 5 参数为新形态 string[]
158
142
  return expectSingleLineArray(goalInitMock.mock.calls[0][4]);
159
143
  }
160
144
 
161
- it("direct isolation: 3 步 plan → 总述 + 3 条 preview", () => {
145
+ it("直接投递:3 步 plan → 总述 + 3 条 preview", () => {
162
146
  fsMock.readFileSync.mockReturnValue(makePlanContent(["Step A", "Step B", "Step C"]));
163
147
 
164
- handlePlanComplete(pi as never, ctx as never, makeActiveState(), "direct", "execute");
148
+ handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
165
149
 
166
150
  expect(getCriteriaArg()).toEqual([
167
- "All 3 steps of plan executed and verified",
151
+ "All 3 steps of login-page executed and verified",
168
152
  "1. Step A",
169
153
  "2. Step B",
170
154
  "3. Step C",
171
155
  ]);
172
156
  });
173
157
 
174
- it("compact isolation: onComplete 后 goalInit 收到同形态数组", () => {
175
- fsMock.readFileSync.mockReturnValue(makePlanContent(["Step one", "Step two"]));
176
-
177
- handlePlanComplete(pi as never, ctx as never, makeActiveState(), "compact", "execute");
178
- ctx._onCompleteFns[0]();
179
-
180
- expect(getCriteriaArg()).toEqual([
181
- "All 2 steps of plan executed and verified",
182
- "1. Step one",
183
- "2. Step two",
184
- ]);
185
- });
186
-
187
- it("12 步 plan → 数组长度固定 4(1 总述 + 3 preview),不再按 8 条上限截断", () => {
188
- const steps = Array.from({ length: 12 }, (_, i) => `Step ${i + 1}`);
189
- fsMock.readFileSync.mockReturnValue(makePlanContent(steps));
190
-
191
- handlePlanComplete(pi as never, ctx as never, makeActiveState(), "direct", "execute");
192
-
193
- const items = getCriteriaArg();
194
- expect(items).toHaveLength(4);
195
- expect(items[0]).toContain("12 steps");
196
- expect(items[0]).toContain("plan");
197
- });
198
-
199
158
  it("CRLF plan 文件 → 每条 criteria 仍单行不含 \\r \\n", () => {
200
159
  fsMock.readFileSync.mockReturnValue("## 实现步骤\r\n1. Step one\r\n2. Step two\r\n3. Step three");
201
160
 
202
- handlePlanComplete(pi as never, ctx as never, makeActiveState(), "direct", "execute");
161
+ handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
203
162
 
204
163
  expect(getCriteriaArg()).toEqual([
205
- "All 3 steps of plan executed and verified",
164
+ "All 3 steps of login-page executed and verified",
206
165
  "1. Step one",
207
166
  "2. Step two",
208
167
  "3. Step three",
@@ -212,7 +171,7 @@ describe("handlePlanComplete — goalInit slot 第 5 参数为新形态 string[]
212
171
  it("0 步 plan → tryGoalInit 提前退出,goalInit 不被调用", () => {
213
172
  (fsMock.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue("## Overview\nNo numbered steps here.");
214
173
 
215
- handlePlanComplete(pi as never, ctx as never, makeActiveState(), "direct", "execute");
174
+ handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
216
175
 
217
176
  expect(goalInitMock).not.toHaveBeenCalled();
218
177
  // steer 仍发出(执行流程不因 goal 缺席中断)——custom message 三要素 + steer options(A6)
@@ -7,12 +7,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
7
7
 
8
8
  import { getLogger } from "@zhushanwen/pi-extension-logger";
9
9
 
10
- import { extractPlanSteps } from "../compact.js";
10
+ import { extractPlanSteps } from "../execution-notice.js";
11
11
  import {
12
12
  formatAvailablePlans,
13
13
  getBuiltinTemplateDir,
14
14
  listTemplates,
15
- loadTemplate,
16
15
  } from "../templates.js";
17
16
 
18
17
  const BUILTIN_TEMPLATE_NAMES = [
@@ -46,7 +45,7 @@ function absentDir(label: string): string {
46
45
  return join(tmpdir(), `plan-absent-${label}-${randomUUID()}`);
47
46
  }
48
47
 
49
- /** loadTemplate 用隔离源(用户级缺位 → 只剩内置源,不受运行机真实 ~/.agents/plans 干扰) */
48
+ /** 发现视图用隔离源(用户级缺位 → 只剩内置源,不受运行机真实 ~/.agents/plans 干扰) */
50
49
  function isolatedSources(): { userPlansDir: string } {
51
50
  return { userPlansDir: absentDir("user") };
52
51
  }
@@ -170,24 +169,6 @@ describe("空发现 warn(bundle 布局事故信号)", () => {
170
169
  });
171
170
  });
172
171
 
173
- describe("loadTemplate(发现视图的读取侧)", () => {
174
- it("returns content for existing builtin template(隔离用户源)", () => {
175
- const content = loadTemplate("feature-plan", isolatedSources());
176
- expect(content).not.toBeNull();
177
- expect(content).toContain("## ");
178
- });
179
-
180
- it("returns null for non-existent template", () => {
181
- expect(loadTemplate("non-existent-template", isolatedSources())).toBeNull();
182
- });
183
-
184
- it("从合并视图解析:同名时用户级胜者内容被加载", () => {
185
- const userDir = mkTmpDir("plan-lt-user-");
186
- writeTemplate(userDir, "feature-plan.md", "# user-owned skeleton\n");
187
- expect(loadTemplate("feature-plan", { userPlansDir: userDir })).toBe("# user-owned skeleton\n");
188
- });
189
- });
190
-
191
172
  describe("<available-plans> 段拼装纯函数(D6)", () => {
192
173
  it("空清单返回空串(不注入段)", () => {
193
174
  expect(formatAvailablePlans([])).toBe("");
@@ -233,12 +214,14 @@ describe("template ↔ extractPlanSteps alignment guard (D4)", () => {
233
214
  // ① 模板标题改名 → 恰一节断言失败;
234
215
  // ② 解析正则与标题脱节 → 提取退化到 fallback 收进其他节的噪音项 → toEqual 失败。
235
216
  it.each(BUILTIN_TEMPLATE_NAMES)("template '%s' has exactly one '## Implementation Steps' section that extractPlanSteps consumes", (name) => {
236
- const content = loadTemplate(name, isolatedSources());
237
- expect(content).not.toBeNull();
217
+ // 胜者 path 直读(读取侧已随 select-template 单次扫描复用收口——不另有按名读取 API)
218
+ const winner = listTemplates(isolatedSources()).find((t) => t.name === name);
219
+ expect(winner).toBeDefined();
220
+ const content = winner ? fs.readFileSync(winner.path, "utf-8") : "";
238
221
 
239
- expect(content!.match(/^## Implementation Steps$/gm)).toHaveLength(1);
222
+ expect(content.match(/^## Implementation Steps$/gm)).toHaveLength(1);
240
223
 
241
- const plan = content!
224
+ const plan = content
242
225
  // 在第一个非步骤节标题后放噪音编号项(模拟 Requirements 等节含编号列表)
243
226
  .replace(/^(## (?!Implementation Steps).+)$/m, "$1\n1. noise-from-other-section")
244
227
  // 在步骤节标题后填编号步骤(模拟 AI 按模板写 plan.md)