Files
paperclip/server/src/__tests__/cursor-local-skill-sync.test.ts
T
2026-03-15 07:05:01 -05:00

142 lines
4.6 KiB
TypeScript

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 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: ["paperclip"],
},
},
} as const;
const before = await listCursorSkills(ctx);
expect(before.mode).toBe("persistent");
expect(before.desiredSkills).toContain("paperclip");
expect(before.entries.find((entry) => entry.name === "paperclip")?.required).toBe(true);
expect(before.entries.find((entry) => entry.name === "paperclip")?.state).toBe("missing");
const after = await syncCursorSkills(ctx, ["paperclip"]);
expect(after.entries.find((entry) => entry.name === "paperclip")?.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: [
{
name: "paperclip",
source: paperclipDir,
required: true,
requiredReason: "Bundled Paperclip skills are always available for local adapters.",
},
{
name: "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.name === "ascii-heart")?.state).toBe("missing");
const after = await syncCursorSkills(ctx, ["ascii-heart"]);
expect(after.warnings).toEqual([]);
expect(after.entries.find((entry) => entry.name === "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: ["paperclip"],
},
},
} as const;
await syncCursorSkills(configuredCtx, ["paperclip"]);
const clearedCtx = {
...configuredCtx,
config: {
env: {
HOME: home,
},
paperclipSkillSync: {
desiredSkills: [],
},
},
} as const;
const after = await syncCursorSkills(clearedCtx, []);
expect(after.desiredSkills).toContain("paperclip");
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
expect((await fs.lstat(path.join(home, ".cursor", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
});
});