1import * as NodeOS from "node:os";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { describe, expect, it } from "vite-plus/test";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8import * as PlatformError from "effect/PlatformError";
9import type * as Scope from "effect/Scope";
10import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
11
12import {
13 discoverCursorSkills,
14 hasCursorSkillMention,
15 probeCursorSkills,
16 rewriteCursorSkillMentions,
17} from "./skills.ts";
18
19const runNode = <A, E>(
20 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | Scope.Scope>,
21) => effect.pipe(Effect.scoped, Effect.provide(NodeServices.layer), Effect.runPromise);
22
23describe("Cursor skills", () => {
24 it("discovers project, user, then built-in skills by precedence", async () =>
25 await runNode(
26 Effect.gen(function* () {
27 const fileSystem = yield* FileSystem.FileSystem;
28 const path = yield* Path.Path;
29 const userHome = yield* fileSystem
30 .makeTempDirectoryScoped({
31 directory: NodeOS.tmpdir(),
32 prefix: "cursor-skills-home-",
33 })
34 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
35 const workspace = yield* fileSystem
36 .makeTempDirectoryScoped({
37 directory: NodeOS.tmpdir(),
38 prefix: "cursor-skills-workspace-",
39 })
40 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
41 const writeSkill = Effect.fn("writeCursorSkill")(function* (
42 root: string,
43 name: string,
44 contents: string,
45 ) {
46 const skillDirectory = path.join(root, name);
47 yield* fileSystem.makeDirectory(skillDirectory, { recursive: true });
48 yield* fileSystem.writeFileString(path.join(skillDirectory, "SKILL.md"), contents);
49 });
50
51 yield* writeSkill(
52 path.join(userHome, ".cursor", "skills"),
53 "review",
54 "---\ndescription: user review\n---\n",
55 );
56 yield* writeSkill(
57 path.join(workspace, ".agents", "skills", "nested"),
58 "review",
59 "---\nname: Review changes\ndescription: project review\n---\n",
60 );
61 yield* writeSkill(
62 path.join(workspace, ".cursor", "skills"),
63 "internal",
64 "---\nuser-invocable: false\n---\n",
65 );
66 yield* writeSkill(
67 path.join(workspace, ".cursor", "skills"),
68 "oversized",
69 "x".repeat(1_000_001),
70 );
71 const builtinRoot = path.join(userHome, ".cursor", "skills-cursor");
72 yield* writeSkill(builtinRoot, "loop", "---\ndescription: builtin loop\n---\n");
73 yield* writeSkill(builtinRoot, "review", "---\ndescription: builtin review\n---\n");
74 yield* writeSkill(builtinRoot, "ide-only", "---\nmetadata:\n surfaces:\n - ide\n---\n");
75 yield* fileSystem.makeDirectory(path.join(userHome, ".codex"), { recursive: true });
76 yield* fileSystem.writeFileString(
77 path.join(userHome, ".codex", "skills"),
78 "not a directory",
79 );
80
81 const skills = yield* discoverCursorSkills(workspace, { HOME: userHome });
82 expect(skills).toEqual([
83 {
84 name: "internal",
85 path: path.join(workspace, ".cursor", "skills", "internal", "SKILL.md"),
86 scope: "project",
87 enabled: true,
88 userInvocable: false,
89 },
90 {
91 name: "loop",
92 description: "builtin loop",
93 path: path.join(builtinRoot, "loop", "SKILL.md"),
94 scope: "system",
95 enabled: true,
96 },
97 {
98 name: "oversized",
99 path: path.join(workspace, ".cursor", "skills", "oversized", "SKILL.md"),
100 scope: "project",
101 enabled: true,
102 },
103 {
104 name: "review",
105 displayName: "Review changes",
106 description: "project review",
107 path: path.join(workspace, ".agents", "skills", "nested", "review", "SKILL.md"),
108 scope: "project",
109 enabled: true,
110 },
111 ]);
112 expect(
113 (yield* probeCursorSkills(workspace, { HOME: userHome }).pipe(Effect.result))._tag,
114 ).toBe("Failure");
115 }),
116 ));
117
118 it("skips a skill tree deeper than the limit instead of failing discovery", async () =>
119 await runNode(
120 Effect.gen(function* () {
121 const fileSystem = yield* FileSystem.FileSystem;
122 const path = yield* Path.Path;
123 const tempDirectory = (prefix: string) =>
124 fileSystem
125 .makeTempDirectoryScoped({ directory: NodeOS.tmpdir(), prefix })
126 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
127 const userHome = yield* tempDirectory("cursor-skills-home-");
128 const workspace = yield* tempDirectory("cursor-skills-workspace-");
129 const writeSkill = Effect.fn("writeCursorSkill")(function* (directory: string) {
130 yield* fileSystem.makeDirectory(directory, { recursive: true });
131 yield* fileSystem.writeFileString(path.join(directory, "SKILL.md"), "---\n---\n");
132 });
133 const userRoot = path.join(userHome, ".claude", "skills");
134 const atLimit = path.join(
135 userRoot,
136 "deep",
137 "2",
138 "3",
139 "4",
140 "5",
141 "6",
142 "7",
143 "8",
144 "9",
145 "deep-skill",
146 );
147
148 yield* writeSkill(path.join(workspace, ".agents", "skills", "review"));
149 yield* writeSkill(atLimit);
150 yield* writeSkill(path.join(atLimit, "below"));
151 yield* writeSkill(path.join(userRoot, "later"));
152
153 const expected = [
154 {
155 name: "deep-skill",
156 path: path.join(atLimit, "SKILL.md"),
157 scope: "user",
158 enabled: true,
159 },
160 {
161 name: "later",
162 path: path.join(userRoot, "later", "SKILL.md"),
163 scope: "user",
164 enabled: true,
165 },
166 {
167 name: "review",
168 path: path.join(workspace, ".agents", "skills", "review", "SKILL.md"),
169 scope: "project",
170 enabled: true,
171 },
172 ];
173 expect(yield* discoverCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
174 expect(yield* probeCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
175 }),
176 ));
177
178 it("finds sibling skills when one skill ships more files than the scan budget", async () =>
179 await runNode(
180 Effect.gen(function* () {
181 const fileSystem = yield* FileSystem.FileSystem;
182 const path = yield* Path.Path;
183 const tempDirectory = (prefix: string) =>
184 fileSystem
185 .makeTempDirectoryScoped({ directory: NodeOS.tmpdir(), prefix })
186 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
187 const userHome = yield* tempDirectory("cursor-skills-home-");
188 const workspace = yield* tempDirectory("cursor-skills-workspace-");
189 const root = path.join(workspace, ".cursor", "skills");
190 const artifacts = path.join(root, "a-large", "artifacts");
191 yield* fileSystem.makeDirectory(artifacts, { recursive: true });
192 yield* fileSystem.writeFileString(path.join(root, "a-large", "SKILL.md"), "---\n---\n");
193 yield* fileSystem.makeDirectory(path.join(root, "b-small"), { recursive: true });
194 yield* fileSystem.writeFileString(path.join(root, "b-small", "SKILL.md"), "---\n---\n");
195 // More ordinary files than the whole-scan entry budget of 10,000.
196 yield* Effect.forEach(
197 Array.from({ length: 11_000 }, (_, index) => path.join(artifacts, `file-${index}.txt`)),
198 (file) => fileSystem.writeFileString(file, ""),
199 { concurrency: 64, discard: true },
200 );
201
202 const expected = [
203 {
204 name: "a-large",
205 path: path.join(root, "a-large", "SKILL.md"),
206 scope: "project",
207 enabled: true,
208 },
209 {
210 name: "b-small",
211 path: path.join(root, "b-small", "SKILL.md"),
212 scope: "project",
213 enabled: true,
214 },
215 ];
216 expect(yield* discoverCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
217 expect(yield* probeCursorSkills(workspace, { HOME: userHome })).toEqual(expected);
218 }),
219 ));
220
221 it("stops a scan of a huge file tree instead of statting every file", async () => {
222 const root = "/workspace/.cursor/skills";
223 let stats = 0;
224 const fileSystem = FileSystem.layerNoop({
225 realPath: (path) => Effect.succeed(path),
226 readDirectory: (path) =>
227 path === root
228 ? Effect.succeed(Array.from({ length: 200_000 }, (_, index) => `file-${index}`))
229 : Effect.succeed([]),
230 stat: (path) => {
231 stats += 1;
232 if (path === root) return Effect.succeed({ type: "Directory" } as FileSystem.File.Info);
233 if (path.startsWith(`${root}/file-`)) {
234 return Effect.succeed({ type: "File" } as FileSystem.File.Info);
235 }
236 return Effect.fail(
237 PlatformError.systemError({ _tag: "NotFound", module: "FileSystem", method: "stat" }),
238 );
239 },
240 });
241
242 const result = await probeCursorSkills("/workspace", { HOME: "/home" }).pipe(
243 Effect.result,
244 Effect.provide(Layer.mergeAll(fileSystem, Path.layer)),
245 Effect.runPromise,
246 );
247
248 expect(result).toMatchObject({
249 _tag: "Failure",
250 failure: { reason: "scan-budget-exhausted" },
251 });
252 expect(stats).toBeGreaterThanOrEqual(100_000);
253 expect(stats).toBeLessThanOrEqual(100_000);
254 });
255
256 it.skipIf(!symlinksSupported)(
257 "treats a symlinked skill outside the root as a package boundary",
258 async () =>
259 await runNode(
260 Effect.gen(function* () {
261 const fileSystem = yield* FileSystem.FileSystem;
262 const path = yield* Path.Path;
263 const userHome = yield* fileSystem.makeTempDirectoryScoped({
264 directory: NodeOS.tmpdir(),
265 prefix: "cursor-skills-home-",
266 });
267 const workspace = yield* fileSystem
268 .makeTempDirectoryScoped({
269 directory: NodeOS.tmpdir(),
270 prefix: "cursor-skills-workspace-",
271 })
272 .pipe(Effect.flatMap((directory) => fileSystem.realPath(directory)));
273 const library = yield* fileSystem.makeTempDirectoryScoped({
274 directory: NodeOS.tmpdir(),
275 prefix: "cursor-skills-library-",
276 });
277 const writeSkill = Effect.fn("writeCursorSkill")(function* (
278 directory: string,
279 contents: string,
280 ) {
281 yield* fileSystem.makeDirectory(directory, { recursive: true });
282 yield* fileSystem.writeFileString(path.join(directory, "SKILL.md"), contents);
283 });
284
285 yield* writeSkill(path.join(library, "shared-review"), "---\ndescription: shared\n---\n");
286 yield* writeSkill(path.join(library, "shared-review", "hidden"), "---\n---\n");
287 const root = path.join(workspace, ".cursor", "skills");
288 yield* fileSystem.makeDirectory(root, { recursive: true });
289 yield* fileSystem.symlink(path.join(library, "shared-review"), path.join(root, "review"));
290
291 const skills = yield* discoverCursorSkills(workspace, { HOME: userHome });
292 expect(skills).toEqual([
293 {
294 name: "review",
295 description: "shared",
296 path: path.join(root, "review", "SKILL.md"),
297 scope: "project",
298 enabled: true,
299 },
300 ]);
301 expect(
302 (yield* probeCursorSkills(workspace, { HOME: userHome }).pipe(Effect.result))._tag,
303 ).toBe("Success");
304 }),
305 ),
306 );
307
308 it("rewrites only discovered skill mentions into Cursor slash invocations", () => {
309 expect(hasCursorSkillMention("use $Review_Pr:V2 here")).toBe(true);
310 expect(hasCursorSkillMention("please $review this")).toBe(true);
311 expect(
312 rewriteCursorSkillMentions("use $review, keep $HOME and 5$review", new Set(["review"])),
313 ).toBe("use $review, keep $HOME and 5$review");
314 expect(rewriteCursorSkillMentions("please $review this", new Set(["review"]))).toBe(
315 "please /review this",
316 );
317 });
318 it("detects and invokes digit-leading Cursor skills without rewriting money", () => {
319 const names = new Set(["2spec", "20k", "100M", "1e6"]);
320 expect(hasCursorSkillMention("use $2spec here")).toBe(true);
321 expect(hasCursorSkillMention("use $2spec here")).toBe(true);
322 expect(rewriteCursorSkillMentions("use $2spec here", names)).toBe("use /2spec here");
323 expect(rewriteCursorSkillMentions("use $2spec here", new Set())).toBe("use $2spec here");
324 for (const text of [
325 "pay $20 tomorrow",
326 "budget $20k here",
327 "cost $100M total",
328 "limit $1e6 here",
329 ]) {
330 expect(hasCursorSkillMention(text)).toBe(false);
331 expect(rewriteCursorSkillMentions(text, names)).toBe(text);
332 }
333 });
334});