Spaces:
Paused
Paused
| import fs from "node:fs/promises"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it } from "vitest"; | |
| import { | |
| listCursorSkills, | |
| syncCursorSkills, | |
| } from "@paperclipai/adapter-cursor-local/server"; | |
| async function makeTempDir(prefix: string): Promise<string> { | |
| return fs.mkdtemp(path.join(os.tmpdir(), prefix)); | |
| } | |
| async function createSkillDir(root: string, name: string) { | |
| const skillDir = path.join(root, name); | |
| await fs.mkdir(skillDir, { recursive: true }); | |
| await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n`, "utf8"); | |
| return skillDir; | |
| } | |
| describe("cursor local skill sync", () => { | |
| const paperclipKey = "paperclipai/paperclip/paperclip"; | |
| const cleanupDirs = new Set<string>(); | |
| afterEach(async () => { | |
| await Promise.all(Array.from(cleanupDirs).map((dir) => fs.rm(dir, { recursive: true, force: true }))); | |
| cleanupDirs.clear(); | |
| }); | |
| it("reports configured Paperclip skills and installs them into the Cursor skills home", async () => { | |
| const home = await makeTempDir("paperclip-cursor-skill-sync-"); | |
| cleanupDirs.add(home); | |
| const ctx = { | |
| agentId: "agent-1", | |
| companyId: "company-1", | |
| adapterType: "cursor", | |
| config: { | |
| env: { | |
| HOME: home, | |
| }, | |
| paperclipSkillSync: { | |
| desiredSkills: [paperclipKey], | |
| }, | |
| }, | |
| } as const; | |
| const before = await listCursorSkills(ctx); | |
| expect(before.mode).toBe("persistent"); | |
| expect(before.desiredSkills).toContain(paperclipKey); | |
| expect(before.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true); | |
| expect(before.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("missing"); | |
| const after = await syncCursorSkills(ctx, [paperclipKey]); | |
| expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed"); | |
| expect((await fs.lstat(path.join(home, ".cursor", "skills", "paperclip"))).isSymbolicLink()).toBe(true); | |
| }); | |
| it("recognizes company-library runtime skills supplied outside the bundled Paperclip directory", async () => { | |
| const home = await makeTempDir("paperclip-cursor-runtime-skills-home-"); | |
| const runtimeSkills = await makeTempDir("paperclip-cursor-runtime-skills-src-"); | |
| cleanupDirs.add(home); | |
| cleanupDirs.add(runtimeSkills); | |
| const paperclipDir = await createSkillDir(runtimeSkills, "paperclip"); | |
| const asciiHeartDir = await createSkillDir(runtimeSkills, "ascii-heart"); | |
| const ctx = { | |
| agentId: "agent-3", | |
| companyId: "company-1", | |
| adapterType: "cursor", | |
| config: { | |
| env: { | |
| HOME: home, | |
| }, | |
| paperclipRuntimeSkills: [ | |
| { | |
| key: "paperclip", | |
| runtimeName: "paperclip", | |
| source: paperclipDir, | |
| required: true, | |
| requiredReason: "Bundled Paperclip skills are always available for local adapters.", | |
| }, | |
| { | |
| key: "ascii-heart", | |
| runtimeName: "ascii-heart", | |
| source: asciiHeartDir, | |
| }, | |
| ], | |
| paperclipSkillSync: { | |
| desiredSkills: ["ascii-heart"], | |
| }, | |
| }, | |
| } as const; | |
| const before = await listCursorSkills(ctx); | |
| expect(before.warnings).toEqual([]); | |
| expect(before.desiredSkills).toEqual(["paperclip", "ascii-heart"]); | |
| expect(before.entries.find((entry) => entry.key === "ascii-heart")?.state).toBe("missing"); | |
| const after = await syncCursorSkills(ctx, ["ascii-heart"]); | |
| expect(after.warnings).toEqual([]); | |
| expect(after.entries.find((entry) => entry.key === "ascii-heart")?.state).toBe("installed"); | |
| expect((await fs.lstat(path.join(home, ".cursor", "skills", "ascii-heart"))).isSymbolicLink()).toBe(true); | |
| }); | |
| it("keeps required bundled Paperclip skills installed even when the desired set is emptied", async () => { | |
| const home = await makeTempDir("paperclip-cursor-skill-prune-"); | |
| cleanupDirs.add(home); | |
| const configuredCtx = { | |
| agentId: "agent-2", | |
| companyId: "company-1", | |
| adapterType: "cursor", | |
| config: { | |
| env: { | |
| HOME: home, | |
| }, | |
| paperclipSkillSync: { | |
| desiredSkills: [paperclipKey], | |
| }, | |
| }, | |
| } as const; | |
| await syncCursorSkills(configuredCtx, [paperclipKey]); | |
| const clearedCtx = { | |
| ...configuredCtx, | |
| config: { | |
| env: { | |
| HOME: home, | |
| }, | |
| paperclipSkillSync: { | |
| desiredSkills: [], | |
| }, | |
| }, | |
| } as const; | |
| const after = await syncCursorSkills(clearedCtx, []); | |
| expect(after.desiredSkills).toContain(paperclipKey); | |
| expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed"); | |
| expect((await fs.lstat(path.join(home, ".cursor", "skills", "paperclip"))).isSymbolicLink()).toBe(true); | |
| }); | |
| }); | |