import { execFileSync } from "node:child_process"; import { promises as fs } from "node:fs"; import os from "node:os"; import path from "node:path"; import { Readable } from "node:stream"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { CompanyPortabilityFileEntry } from "@paperclipai/shared"; const companySvc = { getById: vi.fn(), create: vi.fn(), update: vi.fn(), }; const agentSvc = { list: vi.fn(), create: vi.fn(), update: vi.fn(), }; const accessSvc = { ensureMembership: vi.fn(), listActiveUserMemberships: vi.fn(), copyActiveUserMemberships: vi.fn(), setPrincipalPermission: vi.fn(), }; const projectSvc = { list: vi.fn(), create: vi.fn(), update: vi.fn(), createWorkspace: vi.fn(), listWorkspaces: vi.fn(), }; const issueSvc = { list: vi.fn(), getById: vi.fn(), getByIdentifier: vi.fn(), create: vi.fn(), }; const routineSvc = { list: vi.fn(), getDetail: vi.fn(), create: vi.fn(), createTrigger: vi.fn(), }; const companySkillSvc = { list: vi.fn(), listFull: vi.fn(), readFile: vi.fn(), importPackageFiles: vi.fn(), }; const assetSvc = { getById: vi.fn(), create: vi.fn(), }; const agentInstructionsSvc = { exportFiles: vi.fn(), materializeManagedBundle: vi.fn(), }; vi.mock("../services/companies.js", () => ({ companyService: () => companySvc, })); vi.mock("../services/agents.js", () => ({ agentService: () => agentSvc, })); vi.mock("../services/access.js", () => ({ accessService: () => accessSvc, })); vi.mock("../services/projects.js", () => ({ projectService: () => projectSvc, })); vi.mock("../services/issues.js", () => ({ issueService: () => issueSvc, })); vi.mock("../services/routines.js", () => ({ routineService: () => routineSvc, })); vi.mock("../services/company-skills.js", () => ({ companySkillService: () => companySkillSvc, })); vi.mock("../services/assets.js", () => ({ assetService: () => assetSvc, })); vi.mock("../services/agent-instructions.js", () => ({ agentInstructionsService: () => agentInstructionsSvc, })); vi.mock("../routes/org-chart-svg.js", () => ({ renderOrgChartPng: vi.fn(async () => Buffer.from("png")), })); const { companyPortabilityService, parseGitHubSourceUrl } = await import("../services/company-portability.js"); function asTextFile(entry: CompanyPortabilityFileEntry | undefined) { expect(typeof entry).toBe("string"); return typeof entry === "string" ? entry : ""; } describe("company portability", () => { const paperclipKey = "paperclipai/paperclip/paperclip"; const companyPlaybookKey = "company/company-1/company-playbook"; beforeEach(() => { vi.clearAllMocks(); companySvc.getById.mockResolvedValue({ id: "company-1", name: "Paperclip", description: null, issuePrefix: "PAP", brandColor: "#5c5fff", logoAssetId: null, logoUrl: null, requireBoardApprovalForNewAgents: true, }); agentSvc.list.mockResolvedValue([ { id: "agent-1", name: "ClaudeCoder", status: "idle", role: "engineer", title: "Software Engineer", icon: "code", reportsTo: null, capabilities: "Writes code", adapterType: "claude_local", adapterConfig: { promptTemplate: "You are ClaudeCoder.", paperclipSkillSync: { desiredSkills: [paperclipKey], }, instructionsFilePath: "/tmp/ignored.md", cwd: "/tmp/ignored", command: "/Users/dotta/.local/bin/claude", model: "claude-opus-4-6", env: { ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "secret-1", version: "latest", }, GH_TOKEN: { type: "secret_ref", secretId: "secret-2", version: "latest", }, PATH: { type: "plain", value: "/usr/bin:/bin", }, }, }, runtimeConfig: { heartbeat: { intervalSec: 3600, }, }, budgetMonthlyCents: 0, permissions: { canCreateAgents: false, }, 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([]); projectSvc.createWorkspace.mockResolvedValue(null); projectSvc.listWorkspaces.mockResolvedValue([]); issueSvc.list.mockResolvedValue([]); issueSvc.getById.mockResolvedValue(null); issueSvc.getByIdentifier.mockResolvedValue(null); routineSvc.list.mockResolvedValue([]); routineSvc.getDetail.mockImplementation(async (id: string) => { const rows = await routineSvc.list(); return rows.find((row: { id: string }) => row.id === id) ?? null; }); routineSvc.create.mockImplementation(async (_companyId: string, input: Record) => ({ id: "routine-created", companyId: "company-1", projectId: input.projectId, goalId: null, parentIssueId: null, title: input.title, description: input.description ?? null, assigneeAgentId: input.assigneeAgentId, priority: input.priority ?? "medium", status: input.status ?? "active", concurrencyPolicy: input.concurrencyPolicy ?? "coalesce_if_active", catchUpPolicy: input.catchUpPolicy ?? "skip_missed", createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, lastTriggeredAt: null, lastEnqueuedAt: null, createdAt: new Date(), updatedAt: new Date(), })); routineSvc.createTrigger.mockImplementation(async (_routineId: string, input: Record) => ({ id: "trigger-created", companyId: "company-1", routineId: "routine-created", kind: input.kind, label: input.label ?? null, enabled: input.enabled ?? true, cronExpression: input.kind === "schedule" ? input.cronExpression ?? null : null, timezone: input.kind === "schedule" ? input.timezone ?? null : null, nextRunAt: null, lastFiredAt: null, publicId: null, secretId: null, signingMode: input.kind === "webhook" ? input.signingMode ?? "bearer" : null, replayWindowSec: input.kind === "webhook" ? input.replayWindowSec ?? 300 : null, lastRotatedAt: null, lastResult: null, createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, createdAt: new Date(), updatedAt: new Date(), })); const companySkills = [ { id: "skill-1", companyId: "company-1", key: paperclipKey, 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", key: companyPlaybookKey, 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.list.mockResolvedValue(companySkills); companySkillSvc.listFull.mockResolvedValue(companySkills); 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([]); assetSvc.getById.mockReset(); assetSvc.getById.mockResolvedValue(null); assetSvc.create.mockReset(); accessSvc.setPrincipalPermission.mockResolvedValue(undefined); assetSvc.create.mockResolvedValue({ id: "asset-created", }); accessSvc.listActiveUserMemberships.mockResolvedValue([ { id: "membership-1", companyId: "company-1", principalType: "user", principalId: "user-1", membershipRole: "owner", status: "active", }, ]); accessSvc.copyActiveUserMemberships.mockResolvedValue([]); agentInstructionsSvc.exportFiles.mockImplementation(async (agent: { name: string }) => ({ files: { "AGENTS.md": agent.name === "CMO" ? "You are CMO." : "You are ClaudeCoder." }, entryFile: "AGENTS.md", warnings: [], })); agentInstructionsSvc.materializeManagedBundle.mockImplementation(async (agent: { adapterConfig: Record }) => ({ bundle: null, adapterConfig: { ...agent.adapterConfig, instructionsBundleMode: "managed", instructionsRootPath: `/tmp/${agent.id}`, instructionsEntryFile: "AGENTS.md", instructionsFilePath: `/tmp/${agent.id}/AGENTS.md`, }, })); }); it("parses canonical GitHub import URLs with explicit ref and package path", () => { expect( parseGitHubSourceUrl("https://github.com/paperclipai/companies?ref=feature%2Fdemo&path=gstack"), ).toEqual({ owner: "paperclipai", repo: "companies", ref: "feature/demo", basePath: "gstack", companyPath: "gstack/COMPANY.md", }); }); it("parses canonical GitHub import URLs with explicit companyPath", () => { expect( parseGitHubSourceUrl( "https://github.com/paperclipai/companies?ref=abc123&companyPath=gstack%2FCOMPANY.md", ), ).toEqual({ owner: "paperclipai", repo: "companies", ref: "abc123", basePath: "gstack", companyPath: "gstack/COMPANY.md", }); }); 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", { include: { company: true, agents: true, projects: false, issues: false, }, }); expect(asTextFile(exported.files["COMPANY.md"])).toContain('name: "Paperclip"'); expect(asTextFile(exported.files["COMPANY.md"])).toContain('schema: "agentcompanies/v1"'); expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain("You are ClaudeCoder."); expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain("skills:"); expect(asTextFile(exported.files["agents/claudecoder/AGENTS.md"])).toContain(`- "${paperclipKey}"`); expect(asTextFile(exported.files["agents/cmo/AGENTS.md"])).not.toContain("skills:"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("metadata:"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain('kind: "github-dir"'); expect(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"]).toBeUndefined(); expect(asTextFile(exported.files["skills/company/PAP/company-playbook/SKILL.md"])).toContain("# Company Playbook"); expect(asTextFile(exported.files["skills/company/PAP/company-playbook/references/checklist.md"])).toContain("# Checklist"); const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain('schema: "paperclip/v1"'); expect(extension).not.toContain("promptTemplate"); expect(extension).not.toContain("instructionsFilePath"); expect(extension).not.toContain("command:"); expect(extension).not.toContain("secretId"); expect(extension).not.toContain('type: "secret_ref"'); expect(extension).toContain("inputs:"); expect(extension).toContain("ANTHROPIC_API_KEY:"); expect(extension).toContain('requirement: "optional"'); expect(extension).toContain('default: ""'); expect(extension).not.toContain("paperclipSkillSync"); expect(extension).not.toContain("PATH:"); expect(extension).not.toContain("requireBoardApprovalForNewAgents: true"); expect(extension).not.toContain("budgetMonthlyCents: 0"); expect(exported.warnings).toContain("Agent claudecoder command /Users/dotta/.local/bin/claude was omitted from export because it is system-dependent."); expect(exported.warnings).toContain("Agent claudecoder PATH override was omitted from export because it is system-dependent."); }); it("exports default sidebar order into the Paperclip extension and manifest", async () => { const portability = companyPortabilityService({} as any); projectSvc.list.mockResolvedValue([ { id: "project-2", companyId: "company-1", name: "Zulu", urlKey: "zulu", description: null, leadAgentId: null, targetDate: null, color: null, status: "planned", executionWorkspacePolicy: null, archivedAt: null, workspaces: [], }, { id: "project-1", companyId: "company-1", name: "Alpha", urlKey: "alpha", description: null, leadAgentId: null, targetDate: null, color: null, status: "planned", executionWorkspacePolicy: null, archivedAt: null, workspaces: [], }, ]); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: true, issues: false, }, }); expect(asTextFile(exported.files[".paperclip.yaml"])).toContain([ "sidebar:", " agents:", ' - "claudecoder"', ' - "cmo"', " projects:", ' - "alpha"', ' - "zulu"', ].join("\n")); expect(exported.manifest.sidebar).toEqual({ agents: ["claudecoder", "cmo"], projects: ["alpha", "zulu"], }); }); 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(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("# Paperclip"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"])).toContain("metadata:"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/paperclip/references/api.md"])).toContain("# API"); }); it("exports only selected skills when skills filter is provided", async () => { const portability = companyPortabilityService({} as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, skills: ["company-playbook"], }); expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined(); expect(asTextFile(exported.files["skills/company/PAP/company-playbook/SKILL.md"])).toContain("# Company Playbook"); expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeUndefined(); }); it("warns and exports all skills when skills filter matches nothing", async () => { const portability = companyPortabilityService({} as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, skills: ["nonexistent-skill"], }); expect(exported.warnings).toContainEqual(expect.stringContaining("nonexistent-skill")); expect(exported.files["skills/company/PAP/company-playbook/SKILL.md"]).toBeDefined(); expect(exported.files["skills/paperclipai/paperclip/paperclip/SKILL.md"]).toBeDefined(); }); it("exports the company logo into images/ and references it from .paperclip.yaml", async () => { const storage = { getObject: vi.fn().mockResolvedValue({ stream: Readable.from([Buffer.from("png-bytes")]), }), }; companySvc.getById.mockResolvedValue({ id: "company-1", name: "Paperclip", description: null, issuePrefix: "PAP", brandColor: "#5c5fff", logoAssetId: "logo-1", logoUrl: "/api/assets/logo-1/content", requireBoardApprovalForNewAgents: true, }); assetSvc.getById.mockResolvedValue({ id: "logo-1", companyId: "company-1", objectKey: "assets/companies/logo-1", contentType: "image/png", originalFilename: "logo.png", }); const portability = companyPortabilityService({} as any, storage as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: false, projects: false, issues: false, }, }); expect(storage.getObject).toHaveBeenCalledWith("company-1", "assets/companies/logo-1"); expect(exported.files["images/company-logo.png"]).toEqual({ encoding: "base64", data: Buffer.from("png-bytes").toString("base64"), contentType: "image/png", }); expect(exported.files[".paperclip.yaml"]).toContain('logoPath: "images/company-logo.png"'); }); it("exports duplicate skill slugs into readable namespaced paths", async () => { const portability = companyPortabilityService({} as any); companySkillSvc.readFile.mockImplementation(async (_companyId: string, skillId: string, relativePath: string) => { if (skillId === "skill-local") { return { skillId, path: relativePath, kind: "skill", content: "---\nname: release-changelog\n---\n\n# Local Release Changelog\n", language: "markdown", markdown: true, editable: true, }; } return { skillId, path: relativePath, kind: "skill", content: "---\nname: release-changelog\n---\n\n# Bundled Release Changelog\n", language: "markdown", markdown: true, editable: false, }; }); companySkillSvc.listFull.mockResolvedValue([ { id: "skill-local", companyId: "company-1", key: "local/36dfd631da/release-changelog", slug: "release-changelog", name: "release-changelog", description: "Local release changelog skill", markdown: "---\nname: release-changelog\n---\n\n# Local Release Changelog\n", sourceType: "local_path", sourceLocator: "/tmp/release-changelog", sourceRef: null, trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "local_path", }, }, { id: "skill-paperclip", companyId: "company-1", key: "paperclipai/paperclip/release-changelog", slug: "release-changelog", name: "release-changelog", description: "Bundled release changelog skill", markdown: "---\nname: release-changelog\n---\n\n# Bundled Release Changelog\n", sourceType: "github", sourceLocator: "https://github.com/paperclipai/paperclip/tree/master/skills/release-changelog", sourceRef: "0123456789abcdef0123456789abcdef01234567", trustLevel: "markdown_only", compatibility: "compatible", fileInventory: [{ path: "SKILL.md", kind: "skill" }], metadata: { sourceKind: "paperclip_bundled", owner: "paperclipai", repo: "paperclip", ref: "0123456789abcdef0123456789abcdef01234567", trackingRef: "master", repoSkillDir: "skills/release-changelog", }, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, }); expect(asTextFile(exported.files["skills/local/release-changelog/SKILL.md"])).toContain("# Local Release Changelog"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"])).toContain("metadata:"); expect(asTextFile(exported.files["skills/paperclipai/paperclip/release-changelog/SKILL.md"])).toContain("paperclipai/paperclip/release-changelog"); }); it("builds export previews without tasks by default", async () => { const portability = companyPortabilityService({} as any); projectSvc.list.mockResolvedValue([ { id: "project-1", name: "Launch", urlKey: "launch", description: "Ship it", leadAgentId: "agent-1", targetDate: null, color: null, status: "planned", executionWorkspacePolicy: null, archivedAt: null, }, ]); issueSvc.list.mockResolvedValue([ { id: "issue-1", identifier: "PAP-1", title: "Write launch task", description: "Task body", projectId: "project-1", assigneeAgentId: "agent-1", status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, }, ]); const preview = await portability.previewExport("company-1", { include: { company: true, agents: true, projects: true, }, }); expect(preview.counts.issues).toBe(0); expect(preview.fileInventory.some((entry) => entry.path.startsWith("tasks/"))).toBe(false); }); it("exports portable project workspace metadata and remaps it on import", async () => { const portability = companyPortabilityService({} as any); projectSvc.list.mockResolvedValue([ { id: "project-1", name: "Launch", urlKey: "launch", description: "Ship it", leadAgentId: "agent-1", targetDate: "2026-03-31", color: "#123456", status: "planned", executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", defaultProjectWorkspaceId: "workspace-1", workspaceStrategy: { type: "project_primary", }, }, workspaces: [ { id: "workspace-1", companyId: "company-1", projectId: "project-1", name: "Main Repo", sourceType: "git_repo", cwd: "/Users/dotta/paperclip", repoUrl: "https://github.com/paperclipai/paperclip.git", repoRef: "main", defaultRef: "main", visibility: "default", setupCommand: "pnpm install", cleanupCommand: "rm -rf .paperclip-tmp", remoteProvider: null, remoteWorkspaceRef: null, sharedWorkspaceKey: null, metadata: { language: "typescript", }, isPrimary: true, createdAt: new Date("2026-03-01T00:00:00Z"), updatedAt: new Date("2026-03-01T00:00:00Z"), }, { id: "workspace-2", companyId: "company-1", projectId: "project-1", name: "Local Scratch", sourceType: "local_path", cwd: "/tmp/paperclip-local", repoUrl: null, repoRef: null, defaultRef: null, visibility: "advanced", setupCommand: null, cleanupCommand: null, remoteProvider: null, remoteWorkspaceRef: null, sharedWorkspaceKey: null, metadata: null, isPrimary: false, createdAt: new Date("2026-03-01T00:00:00Z"), updatedAt: new Date("2026-03-01T00:00:00Z"), }, ], archivedAt: null, }, ]); issueSvc.list.mockResolvedValue([ { id: "issue-1", identifier: "PAP-1", title: "Write launch task", description: "Task body", projectId: "project-1", projectWorkspaceId: "workspace-1", assigneeAgentId: "agent-1", status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: { mode: "shared_workspace", }, assigneeAdapterOverrides: null, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: false, projects: true, issues: true, }, }); const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain("workspaces:"); expect(extension).toContain("main-repo:"); expect(extension).toContain('repoUrl: "https://github.com/paperclipai/paperclip.git"'); expect(extension).toContain('defaultProjectWorkspaceKey: "main-repo"'); expect(extension).toContain('projectWorkspaceKey: "main-repo"'); expect(extension).not.toContain("/Users/dotta/paperclip"); expect(extension).not.toContain("workspace-1"); expect(exported.warnings).toContain("Project launch workspace Local Scratch was omitted from export because it does not have a portable repoUrl."); companySvc.create.mockResolvedValue({ id: "company-imported", name: "Imported Paperclip", }); accessSvc.ensureMembership.mockResolvedValue(undefined); agentSvc.list.mockResolvedValue([]); projectSvc.list.mockResolvedValue([]); projectSvc.create.mockResolvedValue({ id: "project-imported", name: "Launch", urlKey: "launch", }); projectSvc.update.mockImplementation(async (projectId: string, data: Record) => ({ id: projectId, name: "Launch", urlKey: "launch", ...data, })); projectSvc.createWorkspace.mockImplementation(async (projectId: string, data: Record) => ({ id: "workspace-imported", companyId: "company-imported", projectId, name: `${data.name ?? "Workspace"}`, sourceType: `${data.sourceType ?? "git_repo"}`, cwd: null, repoUrl: typeof data.repoUrl === "string" ? data.repoUrl : null, repoRef: typeof data.repoRef === "string" ? data.repoRef : null, defaultRef: typeof data.defaultRef === "string" ? data.defaultRef : null, visibility: `${data.visibility ?? "default"}`, setupCommand: typeof data.setupCommand === "string" ? data.setupCommand : null, cleanupCommand: typeof data.cleanupCommand === "string" ? data.cleanupCommand : null, remoteProvider: null, remoteWorkspaceRef: null, sharedWorkspaceKey: null, metadata: (data.metadata as Record | null | undefined) ?? null, isPrimary: Boolean(data.isPrimary), createdAt: new Date("2026-03-02T00:00:00Z"), updatedAt: new Date("2026-03-02T00:00:00Z"), })); issueSvc.create.mockResolvedValue({ id: "issue-imported", title: "Write launch task", }); await portability.importBundle({ source: { type: "inline", rootPath: exported.rootPath, files: exported.files, }, include: { company: true, agents: false, projects: true, issues: true, }, target: { mode: "new_company", newCompanyName: "Imported Paperclip", }, collisionStrategy: "rename", }, "user-1"); expect(projectSvc.createWorkspace).toHaveBeenCalledWith("project-imported", expect.objectContaining({ name: "Main Repo", sourceType: "git_repo", repoUrl: "https://github.com/paperclipai/paperclip.git", repoRef: "main", defaultRef: "main", visibility: "default", })); expect(projectSvc.update).toHaveBeenCalledWith("project-imported", expect.objectContaining({ executionWorkspacePolicy: expect.objectContaining({ enabled: true, defaultMode: "shared_workspace", defaultProjectWorkspaceId: "workspace-imported", }), })); expect(issueSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ projectId: "project-imported", projectWorkspaceId: "workspace-imported", title: "Write launch task", })); }); it("infers portable git metadata from a local checkout without task warning fan-out", async () => { const portability = companyPortabilityService({} as any); const repoDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-portability-git-")); execFileSync("git", ["init"], { cwd: repoDir, stdio: "ignore" }); execFileSync("git", ["checkout", "-b", "main"], { cwd: repoDir, stdio: "ignore" }); execFileSync("git", ["remote", "add", "origin", "https://github.com/paperclipai/paperclip.git"], { cwd: repoDir, stdio: "ignore", }); projectSvc.list.mockResolvedValue([ { id: "project-1", name: "Paperclip App", urlKey: "paperclip-app", description: "Ship it", leadAgentId: null, targetDate: null, color: null, status: "planned", executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", defaultProjectWorkspaceId: "workspace-1", }, workspaces: [ { id: "workspace-1", companyId: "company-1", projectId: "project-1", name: "paperclip", sourceType: "local_path", cwd: repoDir, repoUrl: null, repoRef: null, defaultRef: null, visibility: "default", setupCommand: null, cleanupCommand: null, remoteProvider: null, remoteWorkspaceRef: null, sharedWorkspaceKey: null, metadata: null, isPrimary: true, createdAt: new Date("2026-03-01T00:00:00Z"), updatedAt: new Date("2026-03-01T00:00:00Z"), }, ], archivedAt: null, }, ]); issueSvc.list.mockResolvedValue([ { id: "issue-1", identifier: "PAP-1", title: "Task one", description: "Task body", projectId: "project-1", projectWorkspaceId: "workspace-1", assigneeAgentId: null, status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: false, agents: false, projects: true, issues: true, }, }); const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain('repoUrl: "https://github.com/paperclipai/paperclip.git"'); expect(extension).toContain('projectWorkspaceKey: "paperclip"'); expect(exported.warnings).not.toContainEqual(expect.stringContaining("does not have a portable repoUrl")); expect(exported.warnings).not.toContainEqual(expect.stringContaining("reference workspace workspace-1")); }); it("collapses repeated task workspace warnings into one summary per missing workspace", async () => { const portability = companyPortabilityService({} as any); projectSvc.list.mockResolvedValue([ { id: "project-1", name: "Launch", urlKey: "launch", description: "Ship it", leadAgentId: null, targetDate: null, color: null, status: "planned", executionWorkspacePolicy: null, workspaces: [ { id: "workspace-1", companyId: "company-1", projectId: "project-1", name: "Local Scratch", sourceType: "local_path", cwd: "/tmp/local-only", repoUrl: null, repoRef: null, defaultRef: null, visibility: "default", setupCommand: null, cleanupCommand: null, remoteProvider: null, remoteWorkspaceRef: null, sharedWorkspaceKey: null, metadata: null, isPrimary: true, createdAt: new Date("2026-03-01T00:00:00Z"), updatedAt: new Date("2026-03-01T00:00:00Z"), }, ], archivedAt: null, }, ]); issueSvc.list.mockResolvedValue([ { id: "issue-1", identifier: "PAP-1", title: "Task one", description: null, projectId: "project-1", projectWorkspaceId: "workspace-1", assigneeAgentId: null, status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, }, { id: "issue-2", identifier: "PAP-2", title: "Task two", description: null, projectId: "project-1", projectWorkspaceId: "workspace-1", assigneeAgentId: null, status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, }, { id: "issue-3", identifier: "PAP-3", title: "Task three", description: null, projectId: "project-1", projectWorkspaceId: "workspace-1", assigneeAgentId: null, status: "todo", priority: "medium", labelIds: [], billingCode: null, executionWorkspaceSettings: null, assigneeAdapterOverrides: null, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: false, agents: false, projects: true, issues: true, }, }); expect(exported.warnings).toContain("Project launch workspace Local Scratch was omitted from export because it does not have a portable repoUrl."); expect(exported.warnings).toContain("Tasks pap-1, pap-2, pap-3 reference workspace workspace-1, but that workspace could not be exported portably."); expect(exported.warnings.filter((warning) => warning.includes("workspace reference workspace-1 was omitted from export"))).toHaveLength(0); expect(exported.warnings.filter((warning) => warning.includes("could not be exported portably"))).toHaveLength(1); }); it("reads env inputs back from .paperclip.yaml during preview import", async () => { const portability = companyPortabilityService({} as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, }); const preview = await portability.previewImport({ 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", }); expect(preview.errors).toEqual([]); expect(preview.envInputs).toEqual([ { key: "ANTHROPIC_API_KEY", description: "Provide ANTHROPIC_API_KEY for agent claudecoder", agentSlug: "claudecoder", kind: "secret", requirement: "optional", defaultValue: "", portability: "portable", }, { key: "GH_TOKEN", description: "Provide GH_TOKEN for agent claudecoder", agentSlug: "claudecoder", kind: "secret", requirement: "optional", defaultValue: "", portability: "portable", }, ]); }); it("exports routines as recurring task packages with Paperclip routine extensions", async () => { const portability = companyPortabilityService({} as any); projectSvc.list.mockResolvedValue([ { id: "project-1", name: "Launch", urlKey: "launch", description: "Ship it", leadAgentId: "agent-1", targetDate: null, color: null, status: "planned", executionWorkspacePolicy: null, archivedAt: null, }, ]); routineSvc.list.mockResolvedValue([ { id: "routine-1", companyId: "company-1", projectId: "project-1", goalId: null, parentIssueId: null, title: "Monday Review", description: "Review pipeline health", assigneeAgentId: "agent-1", priority: "high", status: "paused", concurrencyPolicy: "always_enqueue", catchUpPolicy: "enqueue_missed_with_cap", createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, lastTriggeredAt: null, lastEnqueuedAt: null, createdAt: new Date(), updatedAt: new Date(), triggers: [ { id: "trigger-1", companyId: "company-1", routineId: "routine-1", kind: "schedule", label: "Weekly cadence", enabled: true, cronExpression: "0 9 * * 1", timezone: "America/Chicago", nextRunAt: null, lastFiredAt: null, publicId: "public-1", secretId: "secret-1", signingMode: null, replayWindowSec: null, lastRotatedAt: null, lastResult: null, createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, createdAt: new Date(), updatedAt: new Date(), }, { id: "trigger-2", companyId: "company-1", routineId: "routine-1", kind: "webhook", label: "External nudge", enabled: false, cronExpression: null, timezone: null, nextRunAt: null, lastFiredAt: null, publicId: "public-2", secretId: "secret-2", signingMode: "hmac_sha256", replayWindowSec: 120, lastRotatedAt: null, lastResult: null, createdByAgentId: null, createdByUserId: null, updatedByAgentId: null, updatedByUserId: null, createdAt: new Date(), updatedAt: new Date(), }, ], lastRun: null, activeIssue: null, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: true, issues: true, skills: false, }, }); expect(asTextFile(exported.files["tasks/monday-review/TASK.md"])).toContain('recurring: true'); const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain("routines:"); expect(extension).toContain("monday-review:"); expect(extension).toContain('cronExpression: "0 9 * * 1"'); expect(extension).toContain('signingMode: "hmac_sha256"'); expect(extension).not.toContain("secretId"); expect(extension).not.toContain("publicId"); expect(exported.manifest.issues).toEqual([ expect.objectContaining({ slug: "monday-review", recurring: true, status: "paused", priority: "high", routine: expect.objectContaining({ concurrencyPolicy: "always_enqueue", catchUpPolicy: "enqueue_missed_with_cap", triggers: expect.arrayContaining([ expect.objectContaining({ kind: "schedule", cronExpression: "0 9 * * 1", timezone: "America/Chicago" }), expect.objectContaining({ kind: "webhook", enabled: false, signingMode: "hmac_sha256", replayWindowSec: 120 }), ]), }), }), ]); }); it("imports recurring task packages as routines instead of one-time issues", 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", }); projectSvc.create.mockResolvedValue({ id: "project-created", name: "Launch", urlKey: "launch", }); agentSvc.list.mockResolvedValue([]); projectSvc.list.mockResolvedValue([]); const files = { "COMPANY.md": [ "---", 'schema: "agentcompanies/v1"', 'name: "Imported Paperclip"', "---", "", ].join("\n"), "agents/claudecoder/AGENTS.md": [ "---", 'name: "ClaudeCoder"', "---", "", "You write code.", "", ].join("\n"), "projects/launch/PROJECT.md": [ "---", 'name: "Launch"', "---", "", ].join("\n"), "tasks/monday-review/TASK.md": [ "---", 'name: "Monday Review"', 'project: "launch"', 'assignee: "claudecoder"', "recurring: true", "---", "", "Review pipeline health.", "", ].join("\n"), ".paperclip.yaml": [ 'schema: "paperclip/v1"', "routines:", " monday-review:", ' status: "paused"', ' priority: "high"', ' concurrencyPolicy: "always_enqueue"', ' catchUpPolicy: "enqueue_missed_with_cap"', " triggers:", " - kind: schedule", ' cronExpression: "0 9 * * 1"', ' timezone: "America/Chicago"', ' - kind: webhook', ' enabled: false', ' signingMode: "hmac_sha256"', ' replayWindowSec: 120', "", ].join("\n"), }; const preview = await portability.previewImport({ source: { type: "inline", rootPath: "paperclip-demo", files }, include: { company: true, agents: true, projects: true, issues: true, skills: false }, target: { mode: "new_company", newCompanyName: "Imported Paperclip" }, agents: "all", collisionStrategy: "rename", }); expect(preview.errors).toEqual([]); expect(preview.plan.issuePlans).toEqual([ expect.objectContaining({ slug: "monday-review", reason: "Recurring task will be imported as a routine.", }), ]); await portability.importBundle({ source: { type: "inline", rootPath: "paperclip-demo", files }, include: { company: true, agents: true, projects: true, issues: true, skills: false }, target: { mode: "new_company", newCompanyName: "Imported Paperclip" }, agents: "all", collisionStrategy: "rename", }, "user-1"); expect(routineSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ projectId: "project-created", title: "Monday Review", assigneeAgentId: "agent-created", priority: "high", status: "paused", concurrencyPolicy: "always_enqueue", catchUpPolicy: "enqueue_missed_with_cap", }), expect.any(Object)); expect(routineSvc.createTrigger).toHaveBeenCalledTimes(2); expect(routineSvc.createTrigger).toHaveBeenCalledWith("routine-created", expect.objectContaining({ kind: "schedule", cronExpression: "0 9 * * 1", timezone: "America/Chicago", }), expect.any(Object)); expect(routineSvc.createTrigger).toHaveBeenCalledWith("routine-created", expect.objectContaining({ kind: "webhook", enabled: false, signingMode: "hmac_sha256", replayWindowSec: 120, }), expect.any(Object)); expect(issueSvc.create).not.toHaveBeenCalled(); }); it("migrates legacy schedule.recurrence imports into routine triggers", 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", }); projectSvc.create.mockResolvedValue({ id: "project-created", name: "Launch", urlKey: "launch", }); agentSvc.list.mockResolvedValue([]); projectSvc.list.mockResolvedValue([]); const files = { "COMPANY.md": ['---', 'schema: "agentcompanies/v1"', 'name: "Imported Paperclip"', "---", ""].join("\n"), "agents/claudecoder/AGENTS.md": ['---', 'name: "ClaudeCoder"', "---", "", "You write code.", ""].join("\n"), "projects/launch/PROJECT.md": ['---', 'name: "Launch"', "---", ""].join("\n"), "tasks/monday-review/TASK.md": [ "---", 'name: "Monday Review"', 'project: "launch"', 'assignee: "claudecoder"', "schedule:", ' timezone: "America/Chicago"', ' startsAt: "2026-03-16T09:00:00-05:00"', " recurrence:", ' frequency: "weekly"', " interval: 1", " weekdays:", ' - "monday"', "---", "", "Review pipeline health.", "", ].join("\n"), }; const preview = await portability.previewImport({ source: { type: "inline", rootPath: "paperclip-demo", files }, include: { company: true, agents: true, projects: true, issues: true, skills: false }, target: { mode: "new_company", newCompanyName: "Imported Paperclip" }, agents: "all", collisionStrategy: "rename", }); expect(preview.errors).toEqual([]); expect(preview.manifest.issues[0]).toEqual(expect.objectContaining({ recurring: true, legacyRecurrence: expect.objectContaining({ frequency: "weekly" }), })); await portability.importBundle({ source: { type: "inline", rootPath: "paperclip-demo", files }, include: { company: true, agents: true, projects: true, issues: true, skills: false }, target: { mode: "new_company", newCompanyName: "Imported Paperclip" }, agents: "all", collisionStrategy: "rename", }, "user-1"); expect(routineSvc.createTrigger).toHaveBeenCalledWith("routine-created", expect.objectContaining({ kind: "schedule", cronExpression: "0 9 * * 1", timezone: "America/Chicago", }), expect.any(Object)); expect(issueSvc.create).not.toHaveBeenCalled(); }); it("flags recurring task imports that are missing routine-required fields", async () => { const portability = companyPortabilityService({} as any); const preview = await portability.previewImport({ source: { type: "inline", rootPath: "paperclip-demo", files: { "COMPANY.md": ['---', 'schema: "agentcompanies/v1"', 'name: "Imported Paperclip"', "---", ""].join("\n"), "tasks/monday-review/TASK.md": [ "---", 'name: "Monday Review"', "recurring: true", "---", "", "Review pipeline health.", "", ].join("\n"), }, }, include: { company: true, agents: false, projects: false, issues: true, skills: false }, target: { mode: "new_company", newCompanyName: "Imported Paperclip" }, collisionStrategy: "rename", }); expect(preview.errors).toContain("Recurring task monday-review must declare a project to import as a routine."); expect(preview.errors).toContain("Recurring task monday-review must declare an assignee to import as a routine."); }); it("imports a vendor-neutral package without .paperclip.yaml", 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 preview = await portability.previewImport({ source: { type: "inline", rootPath: "paperclip-demo", files: { "COMPANY.md": [ "---", 'schema: "agentcompanies/v1"', 'name: "Imported Paperclip"', 'description: "Portable company package"', "---", "", "# Imported Paperclip", "", ].join("\n"), "agents/claudecoder/AGENTS.md": [ "---", 'name: "ClaudeCoder"', 'title: "Software Engineer"', "---", "", "# ClaudeCoder", "", "You write code.", "", ].join("\n"), }, }, include: { company: true, agents: true, projects: false, issues: false, }, target: { mode: "new_company", newCompanyName: "Imported Paperclip", }, agents: "all", collisionStrategy: "rename", }); expect(preview.errors).toEqual([]); expect(preview.manifest.company?.name).toBe("Imported Paperclip"); expect(preview.manifest.agents).toEqual([ expect.objectContaining({ slug: "claudecoder", name: "ClaudeCoder", adapterType: "process", }), ]); expect(preview.envInputs).toEqual([]); await portability.importBundle({ source: { type: "inline", rootPath: "paperclip-demo", files: { "COMPANY.md": [ "---", 'schema: "agentcompanies/v1"', 'name: "Imported Paperclip"', 'description: "Portable company package"', "---", "", "# Imported Paperclip", "", ].join("\n"), "agents/claudecoder/AGENTS.md": [ "---", 'name: "ClaudeCoder"', 'title: "Software Engineer"', "---", "", "# ClaudeCoder", "", "You write code.", "", ].join("\n"), }, }, include: { company: true, agents: true, projects: false, issues: false, }, target: { mode: "new_company", newCompanyName: "Imported Paperclip", }, agents: "all", collisionStrategy: "rename", }, "user-1"); expect(companySvc.create).toHaveBeenCalledWith(expect.objectContaining({ name: "Imported Paperclip", description: "Portable company package", })); expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ name: "ClaudeCoder", adapterType: "process", })); }); it("treats no-separator auth and api key env names as secrets during export", async () => { const portability = companyPortabilityService({} as any); agentSvc.list.mockResolvedValue([ { id: "agent-1", name: "ClaudeCoder", status: "idle", role: "engineer", title: "Software Engineer", icon: "code", reportsTo: null, capabilities: "Writes code", adapterType: "claude_local", adapterConfig: { promptTemplate: "You are ClaudeCoder.", env: { APIKEY: { type: "plain", value: "sk-plain-api", }, GITHUBAUTH: { type: "plain", value: "gh-auth-token", }, PRIVATEKEY: { type: "plain", value: "private-key-value", }, }, }, runtimeConfig: {}, budgetMonthlyCents: 0, permissions: {}, metadata: null, }, ]); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, }); const extension = asTextFile(exported.files[".paperclip.yaml"]); expect(extension).toContain("APIKEY:"); expect(extension).toContain("GITHUBAUTH:"); expect(extension).toContain("PRIVATEKEY:"); expect(extension).not.toContain("sk-plain-api"); expect(extension).not.toContain("gh-auth-token"); expect(extension).not.toContain("private-key-value"); expect(extension).toContain('kind: "secret"'); }); 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"); const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string")); expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, { onConflict: "replace", }); expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ adapterConfig: expect.objectContaining({ paperclipSkillSync: { desiredSkills: [paperclipKey], }, }), })); }); it("imports a packaged company logo and attaches it to the target company", async () => { const storage = { putFile: vi.fn().mockResolvedValue({ provider: "local_disk", objectKey: "assets/companies/imported-logo", contentType: "image/png", byteSize: 9, sha256: "logo-sha", originalFilename: "company-logo.png", }), }; companySvc.create.mockResolvedValue({ id: "company-imported", name: "Imported Paperclip", logoAssetId: null, }); companySvc.update.mockResolvedValue({ id: "company-imported", name: "Imported Paperclip", logoAssetId: "asset-created", }); agentSvc.create.mockResolvedValue({ id: "agent-created", name: "ClaudeCoder", }); const portability = companyPortabilityService({} as any, storage as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, }); exported.files["images/company-logo.png"] = { encoding: "base64", data: Buffer.from("png-bytes").toString("base64"), contentType: "image/png", }; exported.files[".paperclip.yaml"] = `${exported.files[".paperclip.yaml"]}`.replace( 'brandColor: "#5c5fff"\n', 'brandColor: "#5c5fff"\n logoPath: "images/company-logo.png"\n', ); 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(storage.putFile).toHaveBeenCalledWith(expect.objectContaining({ companyId: "company-imported", namespace: "assets/companies", originalFilename: "company-logo.png", contentType: "image/png", body: Buffer.from("png-bytes"), })); expect(assetSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ objectKey: "assets/companies/imported-logo", contentType: "image/png", createdByUserId: "user-1", })); expect(companySvc.update).toHaveBeenCalledWith("company-imported", { logoAssetId: "asset-created", }); }); it("copies source company memberships for safe new-company imports", async () => { const portability = companyPortabilityService({} as any); companySvc.create.mockResolvedValue({ id: "company-imported", name: "Imported Paperclip", }); 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", }, null, { mode: "agent_safe", sourceCompanyId: "company-1", }); expect(accessSvc.listActiveUserMemberships).toHaveBeenCalledWith("company-1"); expect(accessSvc.copyActiveUserMemberships).toHaveBeenCalledWith("company-1", "company-imported"); expect(accessSvc.ensureMembership).not.toHaveBeenCalledWith("company-imported", "user", expect.anything(), "owner", "active"); const textOnlyFiles = Object.fromEntries(Object.entries(exported.files).filter(([, v]) => typeof v === "string")); expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith("company-imported", textOnlyFiles, { onConflict: "rename", }); }); it("disables timer heartbeats on imported agents", async () => { const portability = companyPortabilityService({} as any); companySvc.create.mockResolvedValue({ id: "company-imported", name: "Imported Paperclip", }); agentSvc.create.mockImplementation(async (_companyId: string, input: Record) => ({ id: `agent-${String(input.name).toLowerCase()}`, name: input.name, adapterConfig: input.adapterConfig, runtimeConfig: input.runtimeConfig, })); 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"); const createdClaude = agentSvc.create.mock.calls.find(([, input]) => input.name === "ClaudeCoder"); expect(createdClaude?.[1]).toMatchObject({ runtimeConfig: { heartbeat: { enabled: false, }, }, }); }); it("imports only selected files and leaves unchecked company metadata alone", async () => { const portability = companyPortabilityService({} as any); const exported = await portability.exportBundle("company-1", { include: { company: true, agents: true, projects: false, issues: false, }, }); agentSvc.list.mockResolvedValue([]); projectSvc.list.mockResolvedValue([]); companySvc.getById.mockResolvedValue({ id: "company-1", name: "Paperclip", description: "Existing company", brandColor: "#123456", requireBoardApprovalForNewAgents: false, }); agentSvc.create.mockResolvedValue({ id: "agent-cmo", name: "CMO", }); const result = await portability.importBundle({ source: { type: "inline", rootPath: exported.rootPath, files: exported.files, }, include: { company: true, agents: true, projects: true, issues: true, }, selectedFiles: ["agents/cmo/AGENTS.md"], target: { mode: "existing_company", companyId: "company-1", }, agents: "all", collisionStrategy: "rename", }, "user-1"); expect(companySvc.update).not.toHaveBeenCalled(); expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith( "company-1", expect.objectContaining({ "COMPANY.md": expect.any(String), "agents/cmo/AGENTS.md": expect.any(String), }), { onConflict: "replace", }, ); expect(companySkillSvc.importPackageFiles).toHaveBeenCalledWith( "company-1", expect.not.objectContaining({ "agents/claudecoder/AGENTS.md": expect.any(String), }), { onConflict: "replace", }, ); expect(agentSvc.create).toHaveBeenCalledTimes(1); expect(agentSvc.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ name: "CMO", runtimeConfig: { heartbeat: { enabled: false, }, }, })); expect(result.company.action).toBe("unchanged"); expect(result.agents).toEqual([ { slug: "cmo", id: "agent-cmo", action: "created", name: "CMO", reason: null, }, ]); }); it("applies adapter overrides while keeping imported AGENTS content implicit", 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", adapterOverrides: { claudecoder: { adapterType: "codex_local", adapterConfig: { dangerouslyBypassApprovalsAndSandbox: true, instructionsFilePath: "/tmp/should-not-survive.md", }, }, }, }, "user-1"); expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ adapterType: "codex_local", adapterConfig: expect.objectContaining({ dangerouslyBypassApprovalsAndSandbox: true, }), })); expect(agentSvc.create).toHaveBeenCalledWith("company-imported", expect.objectContaining({ adapterConfig: expect.not.objectContaining({ instructionsFilePath: expect.anything(), promptTemplate: expect.anything(), }), })); expect(agentInstructionsSvc.materializeManagedBundle).toHaveBeenCalledWith( expect.objectContaining({ name: "ClaudeCoder" }), expect.objectContaining({ "AGENTS.md": expect.stringContaining("You are ClaudeCoder."), }), expect.objectContaining({ clearLegacyPromptTemplate: true, replaceExisting: true, }), ); const materializedFiles = agentInstructionsSvc.materializeManagedBundle.mock.calls[0]?.[1] as Record; expect(materializedFiles["AGENTS.md"]).not.toMatch(/^---\n/); expect(materializedFiles["AGENTS.md"]).not.toContain('name: "ClaudeCoder"'); }); });