@zhushanwen/pi-plan 0.5.2 → 1.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.
- package/README.md +9 -9
- package/package.json +6 -4
- package/skills/plan-ext-config/SKILL.md +33 -18
- package/src/__tests__/active-path.test.ts +204 -0
- package/src/__tests__/command.test.ts +55 -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 +26 -29
- package/src/enter.ts +31 -50
- package/src/{compact.ts → execution-notice.ts} +29 -117
- 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 +663 -255
- 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,397 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
// Mock 共享 logger:降级规格的 warn 留痕可 spy(scheduler importer.test 同款形态)
|
|
8
|
-
const { loggerMock } = vi.hoisted(() => ({
|
|
9
|
-
loggerMock: { debug: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
10
|
-
}));
|
|
11
|
-
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
12
|
-
getLogger: () => loggerMock,
|
|
13
|
-
createLogger: () => loggerMock,
|
|
14
|
-
setPiHandle: vi.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
// 对拍锚点直接用 node_modules 实装(pi 0.84.4 dist,AGENTS.md 语义断言约定)
|
|
18
|
-
import { loadSkillsFromDir } from "@earendil-works/pi-coding-agent";
|
|
19
|
-
|
|
20
|
-
import { detectExecSkills, hasPlanExecMarker, isEnabledByOverrides } from "../exec-skills.js";
|
|
21
|
-
import type { ExecSkill } from "../exec-skills.js";
|
|
22
|
-
|
|
23
|
-
/** SKILL.md 内容构造(frontmatter 字段按需) */
|
|
24
|
-
function skillMd(opts: {
|
|
25
|
-
name?: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
planExec?: boolean;
|
|
28
|
-
disableModelInvocation?: boolean;
|
|
29
|
-
}): string {
|
|
30
|
-
const lines = ["---"];
|
|
31
|
-
if (opts.name !== undefined) lines.push(`name: ${opts.name}`);
|
|
32
|
-
lines.push(`description: ${opts.description ?? "Execute development workflows."}`);
|
|
33
|
-
if (opts.planExec) lines.push("plan-exec: true");
|
|
34
|
-
if (opts.disableModelInvocation) lines.push("disable-model-invocation: true");
|
|
35
|
-
lines.push("---", "", "# Skill body");
|
|
36
|
-
return lines.join("\n");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 四根 fixture 世界:project(.git + 嵌套 cwd + .pi/skills)/ 祖先链 / agentDir / home */
|
|
40
|
-
function makeWorld(): { root: string; cwd: string; agentDir: string; homeDir: string } {
|
|
41
|
-
const root = fs.mkdtempSync(join(tmpdir(), "plan-exec-skills-"));
|
|
42
|
-
const cwd = join(root, "proj", "packages", "lib");
|
|
43
|
-
fs.mkdirSync(join(cwd, ".pi", "skills"), { recursive: true });
|
|
44
|
-
fs.mkdirSync(join(root, "proj", ".git"), { recursive: true });
|
|
45
|
-
fs.mkdirSync(join(root, "proj", "packages", ".agents", "skills"), { recursive: true });
|
|
46
|
-
fs.mkdirSync(join(root, "proj", ".agents", "skills"), { recursive: true });
|
|
47
|
-
const agentDir = join(root, "agent");
|
|
48
|
-
fs.mkdirSync(join(agentDir, "skills"), { recursive: true });
|
|
49
|
-
const homeDir = join(root, "home");
|
|
50
|
-
fs.mkdirSync(join(homeDir, ".agents", "skills"), { recursive: true });
|
|
51
|
-
return { root, cwd, agentDir, homeDir };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 在 dir 下建一个标准 SKILL.md 目录形态 skill(默认带 plan-exec marker) */
|
|
55
|
-
function writeSkill(dir: string, content = skillMd({ planExec: true })): { skillDir: string; skillPath: string } {
|
|
56
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
-
const skillPath = join(dir, "SKILL.md");
|
|
58
|
-
fs.writeFileSync(skillPath, content);
|
|
59
|
-
return { skillDir: dir, skillPath };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function rmWorld(root: string): void {
|
|
63
|
-
fs.rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let world: ReturnType<typeof makeWorld>;
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
vi.clearAllMocks();
|
|
70
|
-
world = makeWorld();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
afterEach(() => {
|
|
74
|
-
vi.unstubAllEnvs();
|
|
75
|
-
rmWorld(world.root);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const detect = (overrides: Partial<Parameters<typeof detectExecSkills>[0]> = {}) =>
|
|
79
|
-
detectExecSkills({
|
|
80
|
-
cwd: world.cwd,
|
|
81
|
-
trusted: true,
|
|
82
|
-
agentDir: world.agentDir,
|
|
83
|
-
homeDir: world.homeDir,
|
|
84
|
-
...overrides,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const namesOf = (skills: ExecSkill[]) => skills.map((s) => s.name);
|
|
88
|
-
|
|
89
|
-
describe("四根枚举与根序(pi 本体加载集对齐)", () => {
|
|
90
|
-
it("enumerates all four roots in pi order: .pi/skills → 祖先链(近→远) → agentDir → ~/.agents/skills", () => {
|
|
91
|
-
writeSkill(join(world.cwd, ".pi", "skills", "lib-skill"));
|
|
92
|
-
writeSkill(join(world.root, "proj", "packages", ".agents", "skills", "mid-skill"));
|
|
93
|
-
writeSkill(join(world.root, "proj", ".agents", "skills", "root-skill"));
|
|
94
|
-
writeSkill(join(world.agentDir, "skills", "agent-skill"));
|
|
95
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "home-skill"));
|
|
96
|
-
|
|
97
|
-
const skills = detect();
|
|
98
|
-
expect(namesOf(skills)).toEqual(["lib-skill", "mid-skill", "root-skill", "agent-skill", "home-skill"]);
|
|
99
|
-
// skillEntryPath 数据通路:steer 指引的路径来源 = 入口文件路径(标准形态即 SKILL.md 路径)
|
|
100
|
-
expect(skills[0].skillEntryPath).toBe(join(world.cwd, ".pi", "skills", "lib-skill", "SKILL.md"));
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("ancestor chain stops at a .git FILE (worktree 双语义), skills beyond the root are not proposed", () => {
|
|
104
|
-
// 独立世界:worktree 形态 .git 文件 + 链外根
|
|
105
|
-
const root = fs.mkdtempSync(join(tmpdir(), "plan-exec-wt-"));
|
|
106
|
-
try {
|
|
107
|
-
fs.mkdirSync(join(root, "wt"), { recursive: true });
|
|
108
|
-
fs.writeFileSync(join(root, "wt", ".git"), "gitdir: /elsewhere\n");
|
|
109
|
-
const cwd = join(root, "wt", "work");
|
|
110
|
-
writeSkill(join(cwd, ".agents", "skills", "inside-skill"));
|
|
111
|
-
writeSkill(join(root, ".agents", "skills", "outside-skill"));
|
|
112
|
-
const skills = detectExecSkills({ cwd, trusted: true, agentDir: join(root, "empty-agent"), homeDir: join(root, "empty-home") });
|
|
113
|
-
expect(namesOf(skills)).toEqual(["inside-skill"]);
|
|
114
|
-
} finally {
|
|
115
|
-
rmWorld(root);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("HOME=tmp 注入:缺省 homeDir 经 process.env.HOME 推导(pi getHomeDir 同式)", () => {
|
|
120
|
-
vi.stubEnv("HOME", world.homeDir);
|
|
121
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "home-skill"));
|
|
122
|
-
const skills = detectExecSkills({ cwd: world.cwd, trusted: true, agentDir: world.agentDir });
|
|
123
|
-
expect(namesOf(skills)).toEqual(["home-skill"]);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("cwd 在 HOME 下时祖先链与 ~/.agents/skills 的同路径项被滤掉(pi :1979 同款,不重复)", () => {
|
|
127
|
-
// 独立世界:home 即 git root,cwd 在其下——链会经过 home/.agents/skills(与第四根同路径)
|
|
128
|
-
const root = fs.mkdtempSync(join(tmpdir(), "plan-exec-dedup-"));
|
|
129
|
-
try {
|
|
130
|
-
const homeDir = join(root, "home");
|
|
131
|
-
fs.mkdirSync(join(homeDir, ".git"), { recursive: true });
|
|
132
|
-
const cwd = join(homeDir, "proj");
|
|
133
|
-
writeSkill(join(homeDir, ".agents", "skills", "home-skill"));
|
|
134
|
-
const skills = detectExecSkills({ cwd, trusted: true, agentDir: join(root, "empty-agent"), homeDir });
|
|
135
|
-
expect(namesOf(skills)).toEqual(["home-skill"]); // 恰一次
|
|
136
|
-
} finally {
|
|
137
|
-
rmWorld(root);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("对照表② overrides-disable 复刻", () => {
|
|
143
|
-
it("project `!glob` excludes by skill dir name (parentName 匹配形态)", () => {
|
|
144
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dev-flow"));
|
|
145
|
-
writeSkill(join(world.cwd, ".pi", "skills", "keep-me"));
|
|
146
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["!dev-*"] }));
|
|
147
|
-
expect(namesOf(detect())).toEqual(["keep-me"]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("project `-exact` excludes by relative path from the root's baseDir", () => {
|
|
151
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dev-flow"));
|
|
152
|
-
// baseDir = <cwd>/.pi,SKILL.md 父目录 rel = skills/dev-flow
|
|
153
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["-skills/dev-flow"] }));
|
|
154
|
-
expect(namesOf(detect())).toEqual([]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("`+` force-include overrides a `!` exclusion, `-` overrides `+`(三级优先级 - > + > !)", () => {
|
|
158
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dev-flow"));
|
|
159
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["!dev-*", "+skills/dev-flow"] }));
|
|
160
|
-
expect(namesOf(detect())).toEqual(["dev-flow"]);
|
|
161
|
-
|
|
162
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["!dev-*", "+skills/dev-flow", "-skills/dev-flow"] }));
|
|
163
|
-
expect(namesOf(detect())).toEqual([]);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("global settings (agentDir) disable user-scope roots; project overrides do not leak into them", () => {
|
|
167
|
-
writeSkill(join(world.agentDir, "skills", "agent-skill"));
|
|
168
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "home-skill"));
|
|
169
|
-
// 项目级 disable 名与 user 根 skill 同名——不作用于 user 根(baseDir 归属隔离)
|
|
170
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["!agent-skill"] }));
|
|
171
|
-
expect(namesOf(detect())).toEqual(["agent-skill", "home-skill"]);
|
|
172
|
-
|
|
173
|
-
fs.writeFileSync(join(world.agentDir, "settings.json"), JSON.stringify({ skills: ["!agent-skill"] }));
|
|
174
|
-
expect(namesOf(detect())).toEqual(["home-skill"]);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("plain patterns without +/-/! prefix are ignored for auto resources(pi getOverridePatterns 同款)", () => {
|
|
178
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dev-flow"));
|
|
179
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), JSON.stringify({ skills: ["dev-flow"] }));
|
|
180
|
-
expect(namesOf(detect())).toEqual(["dev-flow"]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("isEnabledByOverrides 单元:glob 跨形态命中(rel/name/绝对路径/父目录)", () => {
|
|
184
|
-
const baseDir = join(world.root, "base");
|
|
185
|
-
const filePath = join(baseDir, "skills", "dev-flow", "SKILL.md");
|
|
186
|
-
expect(isEnabledByOverrides(filePath, ["!dev-flow"], baseDir)).toBe(false); // parentName
|
|
187
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flow/*"], baseDir)).toBe(false); // rel glob
|
|
188
|
-
expect(isEnabledByOverrides(filePath, ["!**/SKILL.md"], baseDir)).toBe(false); // 跨目录 glob
|
|
189
|
-
expect(isEnabledByOverrides(filePath, [], baseDir)).toBe(true); // 无 overrides 恒启用
|
|
190
|
-
expect(isEnabledByOverrides(filePath, ["!other"], baseDir)).toBe(true);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// globToRegExp 未测分支(minimatch 子集重实现,与 pi 求值的偏差域):? 单字符 /
|
|
194
|
-
// [...] 字符类(含取反)/ 未闭合 ] 字面量 / 正则元字符转义——经公开入口锁语义,
|
|
195
|
-
// 任一分支回归即红(false = 禁用命中,true = 默认启用未命中)。
|
|
196
|
-
it("isEnabledByOverrides 单元:? / 字符类 / 取反 / 未闭合 ] / 元字符转义分支", () => {
|
|
197
|
-
const baseDir = join(world.root, "base");
|
|
198
|
-
const filePath = join(baseDir, "skills", "dev-flow", "SKILL.md");
|
|
199
|
-
// ? = 单字符且不跨 /:dev-flo? 命中 w;dev-flow? 差一字符不命中;skills?dev 不跨 / 不命中
|
|
200
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flo?/SKILL.md"], baseDir)).toBe(false);
|
|
201
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flow?/SKILL.md"], baseDir)).toBe(true);
|
|
202
|
-
expect(isEnabledByOverrides(filePath, ["!skills?dev-flow/SKILL.md"], baseDir)).toBe(true);
|
|
203
|
-
// 字符类:[a-z] 命中 d;[0-9] 未命中
|
|
204
|
-
expect(isEnabledByOverrides(filePath, ["!skills/[a-z]ev-flow/SKILL.md"], baseDir)).toBe(false);
|
|
205
|
-
expect(isEnabledByOverrides(filePath, ["!skills/[0-9]ev-flow/SKILL.md"], baseDir)).toBe(true);
|
|
206
|
-
// 取反类:[!a-z] 排除小写字母 → d 不命中;[!0-9] 排除数字 → d 命中
|
|
207
|
-
expect(isEnabledByOverrides(filePath, ["!skills/[!a-z]ev-flow/SKILL.md"], baseDir)).toBe(true);
|
|
208
|
-
expect(isEnabledByOverrides(filePath, ["!skills/[!0-9]ev-flow/SKILL.md"], baseDir)).toBe(false);
|
|
209
|
-
// 未闭合 ]:[ 按字面量处理(不吞后续段),dev-flow 目录无 [ → 不命中
|
|
210
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flow[/SKILL.md"], baseDir)).toBe(true);
|
|
211
|
-
// 字符类单字:dev-fl[o]w 命中(fl 后是 o);dev-fl[w]w 不命中(fl 后非 w)
|
|
212
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-fl[o]w/SKILL.md"], baseDir)).toBe(false);
|
|
213
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-fl[w]w/SKILL.md"], baseDir)).toBe(true);
|
|
214
|
-
// 正则元字符转义:. 与 $ 不提前终止/锚定匹配——dev.flow(字面点)不命中 dev-flow,
|
|
215
|
-
// dev-flow$(字面 $)不命中;若未转义 . 会通配命中、$ 会锚定改变语义
|
|
216
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev.flow/SKILL.md"], baseDir)).toBe(true);
|
|
217
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flow$/SKILL.md"], baseDir)).toBe(true);
|
|
218
|
-
expect(isEnabledByOverrides(filePath, ["!skills/dev-flow/SKILL.md"], baseDir)).toBe(false);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe("对照表④ trusted 门", () => {
|
|
223
|
-
it("untrusted skips .pi/skills + 祖先链两族,agentDir 与 ~ 两根不受限", () => {
|
|
224
|
-
writeSkill(join(world.cwd, ".pi", "skills", "lib-skill"));
|
|
225
|
-
writeSkill(join(world.root, "proj", ".agents", "skills", "root-skill"));
|
|
226
|
-
writeSkill(join(world.agentDir, "skills", "agent-skill"));
|
|
227
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "home-skill"));
|
|
228
|
-
|
|
229
|
-
expect(namesOf(detect({ trusted: false }))).toEqual(["agent-skill", "home-skill"]);
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe("对照表⑤ 同名 first-writer-wins 去重", () => {
|
|
234
|
-
it("same name across roots: earlier root wins (root 序)", () => {
|
|
235
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dup"));
|
|
236
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "dup"));
|
|
237
|
-
const skills = detect();
|
|
238
|
-
expect(namesOf(skills)).toEqual(["dup"]);
|
|
239
|
-
expect(skills[0].skillEntryPath).toBe(join(world.cwd, ".pi", "skills", "dup", "SKILL.md"));
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("non-marker skill still occupies its name(pi collision 语义:marker 不改变占名)", () => {
|
|
243
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dupe"), skillMd({ planExec: false }));
|
|
244
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "dupe"));
|
|
245
|
-
expect(namesOf(detect())).toEqual([]);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("symlinked duplicate resolves to a single entry(realPath 去重)", () => {
|
|
249
|
-
const real = writeSkill(join(world.cwd, ".pi", "skills", "dup"));
|
|
250
|
-
fs.mkdirSync(join(world.homeDir, ".agents", "skills"), { recursive: true });
|
|
251
|
-
fs.symlinkSync(real.skillDir, join(world.homeDir, ".agents", "skills", "alias"));
|
|
252
|
-
const skills = detect();
|
|
253
|
-
expect(namesOf(skills)).toEqual(["dup"]);
|
|
254
|
-
expect(skills[0].skillEntryPath).toBe(real.skillPath); // 入口文件路径(realPath 去重后指向真身)
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe("对照表①③⑥ 对拍(pi 实装锚点守卫;⑦散 .md 为已登记偏差不设对拍)", () => {
|
|
259
|
-
/**
|
|
260
|
-
* 单根 fixture(挂在 agentDir/skills,其余根不存在):
|
|
261
|
-
* - alpha:frontmatter name 覆盖目录名(⑥ name 派生)
|
|
262
|
-
* - beta:无 frontmatter name → 目录名 fallback(⑥)
|
|
263
|
-
* - no-desc:无 description → ③ 必填门排除
|
|
264
|
-
* - nested/deep/gamma:递归遍历
|
|
265
|
-
* - loose.md:根级散 .md(loadSkillsFromDir 收集,⑦登记面内)
|
|
266
|
-
* - ignored/:.gitignore 排除(①)
|
|
267
|
-
* - node_modules 与 .hidden:loader 内建排除
|
|
268
|
-
*/
|
|
269
|
-
function makeAnchorRoot(): { root: string; agentDir: string } {
|
|
270
|
-
const root = fs.mkdtempSync(join(tmpdir(), "plan-exec-anchor-"));
|
|
271
|
-
const skillsRoot = join(root, "agent", "skills");
|
|
272
|
-
fs.mkdirSync(skillsRoot, { recursive: true });
|
|
273
|
-
writeSkill(join(skillsRoot, "alpha"), skillMd({ name: "custom-alpha", planExec: true }));
|
|
274
|
-
writeSkill(join(skillsRoot, "beta"), skillMd({ planExec: true }));
|
|
275
|
-
writeSkill(join(skillsRoot, "no-desc"), "---\nname: no-desc\n---\n\n# no description\n");
|
|
276
|
-
writeSkill(join(skillsRoot, "nested", "deep", "gamma"), skillMd({ planExec: true }));
|
|
277
|
-
// 根级散 .md(loadSkillsFromDir 收集形态;name 走 frontmatter——散文件 parentDir 名不可用)
|
|
278
|
-
fs.writeFileSync(join(skillsRoot, "loose.md"), skillMd({ name: "loose", planExec: true }));
|
|
279
|
-
writeSkill(join(skillsRoot, "ignored"), skillMd({ planExec: true }));
|
|
280
|
-
writeSkill(join(skillsRoot, "node_modules", "pkg"), skillMd({ planExec: true }));
|
|
281
|
-
writeSkill(join(skillsRoot, ".hidden"), skillMd({ planExec: true }));
|
|
282
|
-
fs.writeFileSync(join(skillsRoot, ".gitignore"), "ignored/\n");
|
|
283
|
-
return { root, agentDir: join(root, "agent") };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
it("pi loadSkillsFromDir anchor:单根规则快照(①ignore/③description 门/⑥name 派生/递归/node_modules/隐藏文件)", () => {
|
|
287
|
-
const { root, agentDir } = makeAnchorRoot();
|
|
288
|
-
try {
|
|
289
|
-
const piResult = loadSkillsFromDir({ dir: join(agentDir, "skills"), source: "anchor" });
|
|
290
|
-
// pi 实装语义快照——pi bump 改单根规则时此处红灯(守卫边界 = 单根规则漂移)
|
|
291
|
-
expect(piResult.skills.map((s) => s.name).sort()).toEqual(["beta", "custom-alpha", "gamma", "loose"]);
|
|
292
|
-
expect(piResult.skills.some((s) => s.name === "no-desc")).toBe(false); // ③ description 必填门
|
|
293
|
-
expect(piResult.skills.some((s) => s.filePath.includes("ignored"))).toBe(false); // ① gitignore
|
|
294
|
-
expect(piResult.skills.some((s) => s.filePath.includes("node_modules"))).toBe(false);
|
|
295
|
-
expect(piResult.skills.some((s) => s.filePath.includes(".hidden"))).toBe(false);
|
|
296
|
-
} finally {
|
|
297
|
-
rmWorld(root);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("detection per-root enumeration equals pi loadSkillsFromDir on the same fixture(对拍等价)", () => {
|
|
302
|
-
const { root, agentDir } = makeAnchorRoot();
|
|
303
|
-
try {
|
|
304
|
-
const piNames = loadSkillsFromDir({ dir: join(agentDir, "skills"), source: "anchor" })
|
|
305
|
-
.skills.map((s) => s.name).sort();
|
|
306
|
-
const detected = detectExecSkills({
|
|
307
|
-
cwd: join(root, "empty-cwd"),
|
|
308
|
-
trusted: true,
|
|
309
|
-
agentDir,
|
|
310
|
-
homeDir: join(root, "empty-home"),
|
|
311
|
-
});
|
|
312
|
-
// marker 过滤前的 pi 加载集 = 检测的单根并集;marker 只减不增
|
|
313
|
-
expect(piNames).toContain("custom-alpha"); // ⑥ frontmatter name
|
|
314
|
-
expect(piNames).toContain("beta");
|
|
315
|
-
expect(namesOf(detected).sort()).toEqual(["beta", "custom-alpha", "gamma", "loose"]);
|
|
316
|
-
// ⑥:steer 指引名与 pi prompt 列表一致(frontmatter name 而非目录名)
|
|
317
|
-
expect(detected.some((s) => s.name === "custom-alpha" && s.skillEntryPath.endsWith(join("alpha", "SKILL.md")))).toBe(true);
|
|
318
|
-
expect(detected.some((s) => s.name === "alpha")).toBe(false);
|
|
319
|
-
} finally {
|
|
320
|
-
rmWorld(root);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("散 .md 形态:skillEntryPath = 文件路径本身(steer 指引路径不悬空,⑦登记面内)", () => {
|
|
325
|
-
const { root, agentDir } = makeAnchorRoot();
|
|
326
|
-
try {
|
|
327
|
-
const detected = detectExecSkills({
|
|
328
|
-
cwd: join(root, "empty-cwd"),
|
|
329
|
-
trusted: true,
|
|
330
|
-
agentDir,
|
|
331
|
-
homeDir: join(root, "empty-home"),
|
|
332
|
-
});
|
|
333
|
-
const loose = detected.find((s) => s.name === "loose");
|
|
334
|
-
// 入口路径 = loose.md 文件本身——不是 `<skills根>/SKILL.md`(散 .md 形态拼 SKILL.md 必然悬空)
|
|
335
|
-
expect(loose?.skillEntryPath).toBe(join(agentDir, "skills", "loose.md"));
|
|
336
|
-
} finally {
|
|
337
|
-
rmWorld(root);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
describe("marker 过滤(plan-exec frontmatter)", () => {
|
|
343
|
-
it("only plan-exec: true is proposed; absent / disable-model-invocation 并存不过滤", () => {
|
|
344
|
-
writeSkill(join(world.cwd, ".pi", "skills", "marked"));
|
|
345
|
-
writeSkill(join(world.cwd, ".pi", "skills", "unmarked"), skillMd({ planExec: false }));
|
|
346
|
-
writeSkill(join(world.cwd, ".pi", "skills", "manual-only"), skillMd({ planExec: true, disableModelInvocation: true }));
|
|
347
|
-
expect(namesOf(detect()).sort()).toEqual(["manual-only", "marked"]); // readdir 字母序,排序断言
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("hasPlanExecMarker strictness: string 'true' does not count as the marker", () => {
|
|
351
|
-
const p = join(world.root, "str-true.md");
|
|
352
|
-
fs.writeFileSync(p, "---\nplan-exec: \"true\"\ndescription: x\n---\n");
|
|
353
|
-
expect(hasPlanExecMarker(p)).toBe(false);
|
|
354
|
-
const q = join(world.root, "bool-true.md");
|
|
355
|
-
fs.writeFileSync(q, "---\nplan-exec: true\ndescription: x\n---\n");
|
|
356
|
-
expect(hasPlanExecMarker(q)).toBe(true);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it("hasPlanExecMarker read failure → false + warn(降级:跳过该项)", () => {
|
|
360
|
-
const warns = loggerMock.warn.mock.calls.length;
|
|
361
|
-
expect(hasPlanExecMarker(join(world.root, "missing.md"))).toBe(false);
|
|
362
|
-
expect(loggerMock.warn.mock.calls.length).toBe(warns + 1);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe("降级规格(检测失败最坏 = 空集,不炸 complete)", () => {
|
|
367
|
-
it("empty world → [](~/.agents/skills 不存在是常态主路径)", () => {
|
|
368
|
-
expect(detect()).toEqual([]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it("EACCES root is skipped without throwing, other roots still detected", () => {
|
|
372
|
-
writeSkill(join(world.cwd, ".pi", "skills", "blocked-skill"));
|
|
373
|
-
writeSkill(join(world.homeDir, ".agents", "skills", "home-skill"));
|
|
374
|
-
fs.chmodSync(join(world.cwd, ".pi", "skills"), 0o000);
|
|
375
|
-
try {
|
|
376
|
-
// pi loadSkillsFromDirInternal 内部吞掉 readdir 错误返回空集(我们外层 try/catch
|
|
377
|
-
// 是纵深防御)——降级语义:该根跳过、不炸、其余根照常
|
|
378
|
-
expect(namesOf(detect())).toEqual(["home-skill"]);
|
|
379
|
-
} finally {
|
|
380
|
-
fs.chmodSync(join(world.cwd, ".pi", "skills"), 0o755);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("broken settings JSON → treated as no overrides(skills 照常检测)", () => {
|
|
385
|
-
writeSkill(join(world.cwd, ".pi", "skills", "dev-flow"));
|
|
386
|
-
fs.writeFileSync(join(world.cwd, ".pi", "settings.json"), "{broken");
|
|
387
|
-
expect(namesOf(detect())).toEqual(["dev-flow"]);
|
|
388
|
-
expect(loggerMock.warn).toHaveBeenCalled();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("broken YAML frontmatter in SKILL.md → skill skipped (pi loader 侧已不加载,检测不炸)", () => {
|
|
392
|
-
// 非法 YAML:pi loadSkillFromFile 解析失败 → 该 skill 不进加载集
|
|
393
|
-
writeSkill(join(world.cwd, ".pi", "skills", "bad-yaml"), "---\nname: [unclosed\ndescription: x\n---\n# body\n");
|
|
394
|
-
writeSkill(join(world.cwd, ".pi", "skills", "fine"));
|
|
395
|
-
expect(namesOf(detect())).toEqual(["fine"]);
|
|
396
|
-
});
|
|
397
|
-
});
|