import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { listCodeBuddySkills, syncCodeBuddySkills, } from "@penclipai/adapter-codebuddy-local/server"; async function makeTempDir(prefix: string): Promise { 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("codebuddy local skill sync", () => { const paperclipKey = "penclipai/paperclip-cn/paperclip"; const cleanupDirs = new Set(); 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 CodeBuddy skills home", async () => { const home = await makeTempDir("paperclip-codebuddy-skill-sync-"); cleanupDirs.add(home); const ctx = { agentId: "agent-1", companyId: "company-1", adapterType: "codebuddy_local", config: { env: { HOME: home, }, paperclipSkillSync: { desiredSkills: [paperclipKey], }, }, } as const; const before = await listCodeBuddySkills(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 syncCodeBuddySkills(ctx, [paperclipKey]); expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed"); await expect(fs.realpath(path.join(home, ".codebuddy", "skills", "paperclip"))).resolves.toMatch(/paperclip$/); }); it("recognizes runtime skills supplied outside the bundled Paperclip directory", async () => { const home = await makeTempDir("paperclip-codebuddy-runtime-skills-home-"); const runtimeSkills = await makeTempDir("paperclip-codebuddy-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: "codebuddy_local", 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 listCodeBuddySkills(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 syncCodeBuddySkills(ctx, ["ascii-heart"]); expect(after.warnings).toEqual([]); expect(after.entries.find((entry) => entry.key === "ascii-heart")?.state).toBe("installed"); expect(await fs.realpath(path.join(home, ".codebuddy", "skills", "ascii-heart"))).toBe( await fs.realpath(asciiHeartDir), ); }); it("keeps required bundled Paperclip skills installed even when the desired set is emptied", async () => { const home = await makeTempDir("paperclip-codebuddy-skill-prune-"); cleanupDirs.add(home); const configuredCtx = { agentId: "agent-2", companyId: "company-1", adapterType: "codebuddy_local", config: { env: { HOME: home, }, paperclipSkillSync: { desiredSkills: [paperclipKey], }, }, } as const; await syncCodeBuddySkills(configuredCtx, [paperclipKey]); const clearedCtx = { ...configuredCtx, config: { env: { HOME: home, }, paperclipSkillSync: { desiredSkills: [], }, }, } as const; const after = await syncCodeBuddySkills(clearedCtx, []); expect(after.desiredSkills).toContain(paperclipKey); expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed"); await expect(fs.realpath(path.join(home, ".codebuddy", "skills", "paperclip"))).resolves.toMatch(/paperclip$/); }); });