Namespace company skill identities
Persist canonical namespaced skill keys, split adapter runtime names from skill keys, and update portability/import flows to carry the canonical identity end-to-end. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
||||
} from "@paperclipai/adapter-claude-local/server";
|
||||
|
||||
describe("claude local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const createAgentKey = "paperclipai/paperclip/paperclip-create-agent";
|
||||
|
||||
it("defaults to mounting all built-in Paperclip skills when no explicit selection exists", async () => {
|
||||
const snapshot = await listClaudeSkills({
|
||||
agentId: "agent-1",
|
||||
@@ -15,9 +18,9 @@ describe("claude local skill sync", () => {
|
||||
|
||||
expect(snapshot.mode).toBe("ephemeral");
|
||||
expect(snapshot.supported).toBe(true);
|
||||
expect(snapshot.desiredSkills).toContain("paperclip");
|
||||
expect(snapshot.entries.find((entry) => entry.name === "paperclip")?.required).toBe(true);
|
||||
expect(snapshot.entries.find((entry) => entry.name === "paperclip")?.state).toBe("configured");
|
||||
expect(snapshot.desiredSkills).toContain(paperclipKey);
|
||||
expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.required).toBe(true);
|
||||
expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
|
||||
});
|
||||
|
||||
it("respects an explicit desired skill list without mutating a persistent home", async () => {
|
||||
@@ -27,13 +30,13 @@ describe("claude local skill sync", () => {
|
||||
adapterType: "claude_local",
|
||||
config: {
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
}, ["paperclip"]);
|
||||
}, [paperclipKey]);
|
||||
|
||||
expect(snapshot.desiredSkills).toContain("paperclip");
|
||||
expect(snapshot.entries.find((entry) => entry.name === "paperclip")?.state).toBe("configured");
|
||||
expect(snapshot.entries.find((entry) => entry.name === "paperclip-create-agent")?.state).toBe("configured");
|
||||
expect(snapshot.desiredSkills).toContain(paperclipKey);
|
||||
expect(snapshot.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("configured");
|
||||
expect(snapshot.entries.find((entry) => entry.key === createAgentKey)?.state).toBe("configured");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ async function createCustomSkill(root: string, skillName: string) {
|
||||
}
|
||||
|
||||
describe("codex local adapter skill injection", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -57,14 +58,18 @@ describe("codex local adapter skill injection", () => {
|
||||
},
|
||||
{
|
||||
skillsHome,
|
||||
skillsEntries: [{ name: "paperclip", source: path.join(currentRepo, "skills", "paperclip") }],
|
||||
skillsEntries: [{
|
||||
key: paperclipKey,
|
||||
runtimeName: "paperclip",
|
||||
source: path.join(currentRepo, "skills", "paperclip"),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
expect(await fs.realpath(path.join(skillsHome, "paperclip"))).toBe(
|
||||
await fs.realpath(path.join(currentRepo, "skills", "paperclip")),
|
||||
);
|
||||
expect(logs.some((line) => line.includes('Repaired Codex skill "paperclip"'))).toBe(true);
|
||||
expect(logs.some((line) => line.includes("Repaired Codex skill"))).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves a custom Codex skill symlink outside Paperclip repo checkouts", async () => {
|
||||
@@ -81,7 +86,11 @@ describe("codex local adapter skill injection", () => {
|
||||
|
||||
await ensureCodexSkillsInjected(async () => {}, {
|
||||
skillsHome,
|
||||
skillsEntries: [{ name: "paperclip", source: path.join(currentRepo, "skills", "paperclip") }],
|
||||
skillsEntries: [{
|
||||
key: paperclipKey,
|
||||
runtimeName: "paperclip",
|
||||
source: path.join(currentRepo, "skills", "paperclip"),
|
||||
}],
|
||||
});
|
||||
|
||||
expect(await fs.realpath(path.join(skillsHome, "paperclip"))).toBe(
|
||||
|
||||
@@ -12,6 +12,7 @@ async function makeTempDir(prefix: string): Promise<string> {
|
||||
}
|
||||
|
||||
describe("codex local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -32,19 +33,19 @@ describe("codex local skill sync", () => {
|
||||
CODEX_HOME: codexHome,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const before = await listCodexSkills(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");
|
||||
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 syncCodexSkills(ctx, ["paperclip"]);
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
const after = await syncCodexSkills(ctx, [paperclipKey]);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(codexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -61,12 +62,12 @@ describe("codex local skill sync", () => {
|
||||
CODEX_HOME: codexHome,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
await syncCodexSkills(configuredCtx, ["paperclip"]);
|
||||
await syncCodexSkills(configuredCtx, [paperclipKey]);
|
||||
|
||||
const clearedCtx = {
|
||||
...configuredCtx,
|
||||
@@ -81,8 +82,8 @@ describe("codex local skill sync", () => {
|
||||
} as const;
|
||||
|
||||
const after = await syncCodexSkills(clearedCtx, []);
|
||||
expect(after.desiredSkills).toContain("paperclip");
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
expect(after.desiredSkills).toContain(paperclipKey);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(codexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,9 @@ vi.mock("../services/company-skills.js", () => ({
|
||||
const { companyPortabilityService } = await import("../services/company-portability.js");
|
||||
|
||||
describe("company portability", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const companyPlaybookKey = "company/company-1/company-playbook";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
companySvc.getById.mockResolvedValue({
|
||||
@@ -86,7 +89,7 @@ describe("company portability", () => {
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are ClaudeCoder.",
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
instructionsFilePath: "/tmp/ignored.md",
|
||||
cwd: "/tmp/ignored",
|
||||
@@ -153,6 +156,7 @@ describe("company portability", () => {
|
||||
{
|
||||
id: "skill-1",
|
||||
companyId: "company-1",
|
||||
key: paperclipKey,
|
||||
slug: "paperclip",
|
||||
name: "paperclip",
|
||||
description: "Paperclip coordination skill",
|
||||
@@ -178,6 +182,7 @@ describe("company portability", () => {
|
||||
{
|
||||
id: "skill-2",
|
||||
companyId: "company-1",
|
||||
key: companyPlaybookKey,
|
||||
slug: "company-playbook",
|
||||
name: "company-playbook",
|
||||
description: "Internal company skill",
|
||||
@@ -244,13 +249,13 @@ describe("company portability", () => {
|
||||
expect(exported.files["COMPANY.md"]).toContain('schema: "agentcompanies/v1"');
|
||||
expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain("You are ClaudeCoder.");
|
||||
expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain("skills:");
|
||||
expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain('- "paperclip"');
|
||||
expect(exported.files["agents/claudecoder/AGENTS.md"]).toContain(`- "${paperclipKey}"`);
|
||||
expect(exported.files["agents/cmo/AGENTS.md"]).not.toContain("skills:");
|
||||
expect(exported.files["skills/paperclip/SKILL.md"]).toContain("metadata:");
|
||||
expect(exported.files["skills/paperclip/SKILL.md"]).toContain('kind: "github-dir"');
|
||||
expect(exported.files["skills/paperclip/references/api.md"]).toBeUndefined();
|
||||
expect(exported.files["skills/company-playbook/SKILL.md"]).toContain("# Company Playbook");
|
||||
expect(exported.files["skills/company-playbook/references/checklist.md"]).toContain("# Checklist");
|
||||
expect(exported.files[`skills/${paperclipKey}/SKILL.md`]).toContain("metadata:");
|
||||
expect(exported.files[`skills/${paperclipKey}/SKILL.md`]).toContain('kind: "github-dir"');
|
||||
expect(exported.files[`skills/${paperclipKey}/references/api.md`]).toBeUndefined();
|
||||
expect(exported.files[`skills/${companyPlaybookKey}/SKILL.md`]).toContain("# Company Playbook");
|
||||
expect(exported.files[`skills/${companyPlaybookKey}/references/checklist.md`]).toContain("# Checklist");
|
||||
|
||||
const extension = exported.files[".paperclip.yaml"];
|
||||
expect(extension).toContain('schema: "paperclip/v1"');
|
||||
@@ -284,9 +289,9 @@ describe("company portability", () => {
|
||||
expandReferencedSkills: true,
|
||||
});
|
||||
|
||||
expect(exported.files["skills/paperclip/SKILL.md"]).toContain("# Paperclip");
|
||||
expect(exported.files["skills/paperclip/SKILL.md"]).toContain("metadata:");
|
||||
expect(exported.files["skills/paperclip/references/api.md"]).toContain("# API");
|
||||
expect(exported.files[`skills/${paperclipKey}/SKILL.md`]).toContain("# Paperclip");
|
||||
expect(exported.files[`skills/${paperclipKey}/SKILL.md`]).toContain("metadata:");
|
||||
expect(exported.files[`skills/${paperclipKey}/references/api.md`]).toContain("# API");
|
||||
});
|
||||
|
||||
it("reads env inputs back from .paperclip.yaml during preview import", async () => {
|
||||
@@ -392,7 +397,7 @@ describe("company portability", () => {
|
||||
expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({
|
||||
adapterConfig: expect.objectContaining({
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -19,6 +19,7 @@ async function createSkillDir(root: string, name: string) {
|
||||
}
|
||||
|
||||
describe("cursor local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -39,19 +40,19 @@ describe("cursor local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} 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");
|
||||
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, ["paperclip"]);
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -74,13 +75,15 @@ describe("cursor local skill sync", () => {
|
||||
},
|
||||
paperclipRuntimeSkills: [
|
||||
{
|
||||
name: "paperclip",
|
||||
key: "paperclip",
|
||||
runtimeName: "paperclip",
|
||||
source: paperclipDir,
|
||||
required: true,
|
||||
requiredReason: "Bundled Paperclip skills are always available for local adapters.",
|
||||
},
|
||||
{
|
||||
name: "ascii-heart",
|
||||
key: "ascii-heart",
|
||||
runtimeName: "ascii-heart",
|
||||
source: asciiHeartDir,
|
||||
},
|
||||
],
|
||||
@@ -93,11 +96,11 @@ describe("cursor local skill sync", () => {
|
||||
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");
|
||||
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.name === "ascii-heart")?.state).toBe("installed");
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -114,12 +117,12 @@ describe("cursor local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
await syncCursorSkills(configuredCtx, ["paperclip"]);
|
||||
await syncCursorSkills(configuredCtx, [paperclipKey]);
|
||||
|
||||
const clearedCtx = {
|
||||
...configuredCtx,
|
||||
@@ -134,8 +137,8 @@ describe("cursor local skill sync", () => {
|
||||
} as const;
|
||||
|
||||
const after = await syncCursorSkills(clearedCtx, []);
|
||||
expect(after.desiredSkills).toContain("paperclip");
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ async function makeTempDir(prefix: string): Promise<string> {
|
||||
}
|
||||
|
||||
describe("gemini local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -32,19 +33,19 @@ describe("gemini local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const before = await listGeminiSkills(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");
|
||||
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 syncGeminiSkills(ctx, ["paperclip"]);
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
const after = await syncGeminiSkills(ctx, [paperclipKey]);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".gemini", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -61,12 +62,12 @@ describe("gemini local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
await syncGeminiSkills(configuredCtx, ["paperclip"]);
|
||||
await syncGeminiSkills(configuredCtx, [paperclipKey]);
|
||||
|
||||
const clearedCtx = {
|
||||
...configuredCtx,
|
||||
@@ -81,8 +82,8 @@ describe("gemini local skill sync", () => {
|
||||
} as const;
|
||||
|
||||
const after = await syncGeminiSkills(clearedCtx, []);
|
||||
expect(after.desiredSkills).toContain("paperclip");
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
expect(after.desiredSkills).toContain(paperclipKey);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".gemini", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ async function makeTempDir(prefix: string): Promise<string> {
|
||||
}
|
||||
|
||||
describe("opencode local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -32,7 +33,7 @@ describe("opencode local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -40,12 +41,12 @@ describe("opencode local skill sync", () => {
|
||||
const before = await listOpenCodeSkills(ctx);
|
||||
expect(before.mode).toBe("persistent");
|
||||
expect(before.warnings).toContain("OpenCode currently uses the shared Claude skills home (~/.claude/skills).");
|
||||
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");
|
||||
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 syncOpenCodeSkills(ctx, ["paperclip"]);
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
const after = await syncOpenCodeSkills(ctx, [paperclipKey]);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".claude", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -62,12 +63,12 @@ describe("opencode local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
await syncOpenCodeSkills(configuredCtx, ["paperclip"]);
|
||||
await syncOpenCodeSkills(configuredCtx, [paperclipKey]);
|
||||
|
||||
const clearedCtx = {
|
||||
...configuredCtx,
|
||||
@@ -82,8 +83,8 @@ describe("opencode local skill sync", () => {
|
||||
} as const;
|
||||
|
||||
const after = await syncOpenCodeSkills(clearedCtx, []);
|
||||
expect(after.desiredSkills).toContain("paperclip");
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
expect(after.desiredSkills).toContain(paperclipKey);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".claude", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,8 @@ describe("paperclip skill utils", () => {
|
||||
|
||||
const entries = await listPaperclipSkillEntries(moduleDir);
|
||||
|
||||
expect(entries.map((entry) => entry.name)).toEqual(["paperclip"]);
|
||||
expect(entries.map((entry) => entry.key)).toEqual(["paperclipai/paperclip/paperclip"]);
|
||||
expect(entries.map((entry) => entry.runtimeName)).toEqual(["paperclip"]);
|
||||
expect(entries[0]?.source).toBe(path.join(root, "skills", "paperclip"));
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ async function makeTempDir(prefix: string): Promise<string> {
|
||||
}
|
||||
|
||||
describe("pi local skill sync", () => {
|
||||
const paperclipKey = "paperclipai/paperclip/paperclip";
|
||||
const cleanupDirs = new Set<string>();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -32,19 +33,19 @@ describe("pi local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const before = await listPiSkills(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");
|
||||
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 syncPiSkills(ctx, ["paperclip"]);
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
const after = await syncPiSkills(ctx, [paperclipKey]);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".pi", "agent", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -61,12 +62,12 @@ describe("pi local skill sync", () => {
|
||||
HOME: home,
|
||||
},
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
desiredSkills: [paperclipKey],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
await syncPiSkills(configuredCtx, ["paperclip"]);
|
||||
await syncPiSkills(configuredCtx, [paperclipKey]);
|
||||
|
||||
const clearedCtx = {
|
||||
...configuredCtx,
|
||||
@@ -81,8 +82,8 @@ describe("pi local skill sync", () => {
|
||||
} as const;
|
||||
|
||||
const after = await syncPiSkills(clearedCtx, []);
|
||||
expect(after.desiredSkills).toContain("paperclip");
|
||||
expect(after.entries.find((entry) => entry.name === "paperclip")?.state).toBe("installed");
|
||||
expect(after.desiredSkills).toContain(paperclipKey);
|
||||
expect(after.entries.find((entry) => entry.key === paperclipKey)?.state).toBe("installed");
|
||||
expect((await fs.lstat(path.join(home, ".pi", "agent", "skills", "paperclip"))).isSymbolicLink()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user