Refine portability export behavior and skill plans
This commit is contained in:
@@ -29,6 +29,12 @@ const issueSvc = {
|
||||
create: vi.fn(),
|
||||
};
|
||||
|
||||
const companySkillSvc = {
|
||||
list: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
importPackageFiles: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("../services/companies.js", () => ({
|
||||
companyService: () => companySvc,
|
||||
}));
|
||||
@@ -49,6 +55,10 @@ vi.mock("../services/issues.js", () => ({
|
||||
issueService: () => issueSvc,
|
||||
}));
|
||||
|
||||
vi.mock("../services/company-skills.js", () => ({
|
||||
companySkillService: () => companySkillSvc,
|
||||
}));
|
||||
|
||||
const { companyPortabilityService } = await import("../services/company-portability.js");
|
||||
|
||||
describe("company portability", () => {
|
||||
@@ -74,6 +84,9 @@ describe("company portability", () => {
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are ClaudeCoder.",
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
},
|
||||
instructionsFilePath: "/tmp/ignored.md",
|
||||
cwd: "/tmp/ignored",
|
||||
command: "/Users/dotta/.local/bin/claude",
|
||||
@@ -106,14 +119,113 @@ describe("company portability", () => {
|
||||
},
|
||||
metadata: null,
|
||||
},
|
||||
{
|
||||
id: "agent-2",
|
||||
name: "CMO",
|
||||
status: "idle",
|
||||
role: "cmo",
|
||||
title: "Chief Marketing Officer",
|
||||
icon: "globe",
|
||||
reportsTo: null,
|
||||
capabilities: "Owns marketing",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are CMO.",
|
||||
},
|
||||
runtimeConfig: {
|
||||
heartbeat: {
|
||||
intervalSec: 3600,
|
||||
},
|
||||
},
|
||||
budgetMonthlyCents: 0,
|
||||
permissions: {
|
||||
canCreateAgents: false,
|
||||
},
|
||||
metadata: null,
|
||||
},
|
||||
]);
|
||||
projectSvc.list.mockResolvedValue([]);
|
||||
issueSvc.list.mockResolvedValue([]);
|
||||
issueSvc.getById.mockResolvedValue(null);
|
||||
issueSvc.getByIdentifier.mockResolvedValue(null);
|
||||
companySkillSvc.list.mockResolvedValue([
|
||||
{
|
||||
id: "skill-1",
|
||||
companyId: "company-1",
|
||||
slug: "paperclip",
|
||||
name: "paperclip",
|
||||
description: "Paperclip coordination skill",
|
||||
markdown: "---\nname: paperclip\ndescription: Paperclip coordination skill\n---\n\n# Paperclip\n",
|
||||
sourceType: "github",
|
||||
sourceLocator: "https://github.com/paperclipai/paperclip/tree/master/skills/paperclip",
|
||||
sourceRef: "0123456789abcdef0123456789abcdef01234567",
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [
|
||||
{ path: "SKILL.md", kind: "skill" },
|
||||
{ path: "references/api.md", kind: "reference" },
|
||||
],
|
||||
metadata: {
|
||||
sourceKind: "github",
|
||||
owner: "paperclipai",
|
||||
repo: "paperclip",
|
||||
ref: "0123456789abcdef0123456789abcdef01234567",
|
||||
trackingRef: "master",
|
||||
repoSkillDir: "skills/paperclip",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "skill-2",
|
||||
companyId: "company-1",
|
||||
slug: "company-playbook",
|
||||
name: "company-playbook",
|
||||
description: "Internal company skill",
|
||||
markdown: "---\nname: company-playbook\ndescription: Internal company skill\n---\n\n# Company Playbook\n",
|
||||
sourceType: "local_path",
|
||||
sourceLocator: "/tmp/company-playbook",
|
||||
sourceRef: null,
|
||||
trustLevel: "markdown_only",
|
||||
compatibility: "compatible",
|
||||
fileInventory: [
|
||||
{ path: "SKILL.md", kind: "skill" },
|
||||
{ path: "references/checklist.md", kind: "reference" },
|
||||
],
|
||||
metadata: {
|
||||
sourceKind: "local_path",
|
||||
},
|
||||
},
|
||||
]);
|
||||
companySkillSvc.readFile.mockImplementation(async (_companyId: string, skillId: string, relativePath: string) => {
|
||||
if (skillId === "skill-2") {
|
||||
return {
|
||||
skillId,
|
||||
path: relativePath,
|
||||
kind: relativePath === "SKILL.md" ? "skill" : "reference",
|
||||
content: relativePath === "SKILL.md"
|
||||
? "---\nname: company-playbook\ndescription: Internal company skill\n---\n\n# Company Playbook\n"
|
||||
: "# Checklist\n",
|
||||
language: "markdown",
|
||||
markdown: true,
|
||||
editable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
skillId,
|
||||
path: relativePath,
|
||||
kind: relativePath === "SKILL.md" ? "skill" : "reference",
|
||||
content: relativePath === "SKILL.md"
|
||||
? "---\nname: paperclip\ndescription: Paperclip coordination skill\n---\n\n# Paperclip\n"
|
||||
: "# API\n",
|
||||
language: "markdown",
|
||||
markdown: true,
|
||||
editable: false,
|
||||
};
|
||||
});
|
||||
companySkillSvc.importPackageFiles.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("exports a clean base package with sanitized Paperclip extension data", async () => {
|
||||
it("exports referenced skills as stubs by default with sanitized Paperclip extension data", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
@@ -128,6 +240,14 @@ describe("company portability", () => {
|
||||
expect(exported.files["COMPANY.md"]).toContain('name: "Paperclip"');
|
||||
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/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");
|
||||
|
||||
const extension = exported.files[".paperclip.yaml"];
|
||||
expect(extension).toContain('schema: "paperclip/v1"');
|
||||
@@ -147,6 +267,24 @@ describe("company portability", () => {
|
||||
expect(exported.warnings).toContain("Agent claudecoder PATH override was omitted from export because it is system-dependent.");
|
||||
});
|
||||
|
||||
it("expands referenced skills when requested", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
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");
|
||||
});
|
||||
|
||||
it("reads env inputs back from .paperclip.yaml during preview import", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
@@ -201,4 +339,58 @@ describe("company portability", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("imports packaged skills and restores desired skill refs on agents", async () => {
|
||||
const portability = companyPortabilityService({} as any);
|
||||
|
||||
companySvc.create.mockResolvedValue({
|
||||
id: "company-imported",
|
||||
name: "Imported Paperclip",
|
||||
});
|
||||
accessSvc.ensureMembership.mockResolvedValue(undefined);
|
||||
agentSvc.create.mockResolvedValue({
|
||||
id: "agent-created",
|
||||
name: "ClaudeCoder",
|
||||
});
|
||||
|
||||
const exported = await portability.exportBundle("company-1", {
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
});
|
||||
|
||||
agentSvc.list.mockResolvedValue([]);
|
||||
|
||||
await portability.importBundle({
|
||||
source: {
|
||||
type: "inline",
|
||||
rootPath: exported.rootPath,
|
||||
files: exported.files,
|
||||
},
|
||||
include: {
|
||||
company: true,
|
||||
agents: true,
|
||||
projects: false,
|
||||
issues: false,
|
||||
},
|
||||
target: {
|
||||
mode: "new_company",
|
||||
newCompanyName: "Imported Paperclip",
|
||||
},
|
||||
agents: "all",
|
||||
collisionStrategy: "rename",
|
||||
}, "user-1");
|
||||
|
||||
expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", exported.files);
|
||||
expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({
|
||||
adapterConfig: expect.objectContaining({
|
||||
paperclipSkillSync: {
|
||||
desiredSkills: ["paperclip"],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user