| 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<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("codebuddy local skill sync", () => { |
| const paperclipKey = "penclipai/paperclip-cn/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 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$/); |
| }); |
| }); |
|
|