2026-03-19 07:24:04 -05:00
import { Readable } from "node:stream" ;
2026-03-14 09:46:16 -05:00
import { beforeEach , describe , expect , it , vi } from "vitest" ;
2026-03-19 07:28:26 -05:00
import type { CompanyPortabilityFileEntry } from "@paperclipai/shared" ;
2026-03-14 09:46:16 -05:00
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 ( ) ,
2026-03-18 21:54:10 -05:00
listActiveUserMemberships : vi.fn ( ) ,
copyActiveUserMemberships : vi.fn ( ) ,
2026-03-14 09:46:16 -05:00
} ;
const projectSvc = {
list : vi.fn ( ) ,
create : vi.fn ( ) ,
update : vi.fn ( ) ,
} ;
const issueSvc = {
list : vi.fn ( ) ,
getById : vi.fn ( ) ,
getByIdentifier : vi.fn ( ) ,
create : vi.fn ( ) ,
} ;
2026-03-14 18:59:26 -05:00
const companySkillSvc = {
list : vi.fn ( ) ,
2026-03-16 17:45:28 -05:00
listFull : vi.fn ( ) ,
2026-03-14 18:59:26 -05:00
readFile : vi.fn ( ) ,
importPackageFiles : vi.fn ( ) ,
} ;
2026-03-19 07:24:04 -05:00
const assetSvc = {
getById : vi.fn ( ) ,
create : vi.fn ( ) ,
} ;
2026-03-17 13:42:00 -05:00
const agentInstructionsSvc = {
exportFiles : vi.fn ( ) ,
materializeManagedBundle : vi.fn ( ) ,
} ;
2026-03-14 09:46:16 -05:00
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 ,
} ) ) ;
2026-03-14 18:59:26 -05:00
vi . mock ( "../services/company-skills.js" , ( ) = > ( {
companySkillService : ( ) = > companySkillSvc ,
} ) ) ;
2026-03-19 07:24:04 -05:00
vi . mock ( "../services/assets.js" , ( ) = > ( {
assetService : ( ) = > assetSvc ,
} ) ) ;
2026-03-17 13:42:00 -05:00
vi . mock ( "../services/agent-instructions.js" , ( ) = > ( {
agentInstructionsService : ( ) = > agentInstructionsSvc ,
} ) ) ;
2026-03-14 09:46:16 -05:00
const { companyPortabilityService } = await import ( "../services/company-portability.js" ) ;
2026-03-19 07:28:26 -05:00
function asTextFile ( entry : CompanyPortabilityFileEntry | undefined ) {
expect ( typeof entry ) . toBe ( "string" ) ;
return typeof entry === "string" ? entry : "" ;
}
2026-03-14 09:46:16 -05:00
describe ( "company portability" , ( ) = > {
2026-03-16 18:27:20 -05:00
const paperclipKey = "paperclipai/paperclip/paperclip" ;
const companyPlaybookKey = "company/company-1/company-playbook" ;
2026-03-14 09:46:16 -05:00
beforeEach ( ( ) = > {
vi . clearAllMocks ( ) ;
companySvc . getById . mockResolvedValue ( {
id : "company-1" ,
name : "Paperclip" ,
description : null ,
2026-03-18 16:54:25 -05:00
issuePrefix : "PAP" ,
2026-03-14 09:46:16 -05:00
brandColor : "#5c5fff" ,
2026-03-19 07:24:04 -05:00
logoAssetId : null ,
logoUrl : null ,
2026-03-14 09:46:16 -05:00
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." ,
2026-03-14 18:59:26 -05:00
paperclipSkillSync : {
2026-03-16 18:27:20 -05:00
desiredSkills : [ paperclipKey ] ,
2026-03-14 18:59:26 -05:00
} ,
2026-03-14 09:46:16 -05:00
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 ,
} ,
2026-03-14 18:59:26 -05:00
{
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 ,
} ,
2026-03-14 09:46:16 -05:00
] ) ;
projectSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . list . mockResolvedValue ( [ ] ) ;
issueSvc . getById . mockResolvedValue ( null ) ;
issueSvc . getByIdentifier . mockResolvedValue ( null ) ;
2026-03-16 17:45:28 -05:00
const companySkills = [
2026-03-14 18:59:26 -05:00
{
id : "skill-1" ,
companyId : "company-1" ,
2026-03-16 18:27:20 -05:00
key : paperclipKey ,
2026-03-14 18:59:26 -05:00
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" ,
2026-03-16 18:27:20 -05:00
key : companyPlaybookKey ,
2026-03-14 18:59:26 -05:00
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" ,
} ,
} ,
2026-03-16 17:45:28 -05:00
] ;
companySkillSvc . list . mockResolvedValue ( companySkills ) ;
companySkillSvc . listFull . mockResolvedValue ( companySkills ) ;
2026-03-14 18:59:26 -05:00
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 ( [ ] ) ;
2026-03-19 07:24:04 -05:00
assetSvc . getById . mockReset ( ) ;
assetSvc . getById . mockResolvedValue ( null ) ;
assetSvc . create . mockReset ( ) ;
assetSvc . create . mockResolvedValue ( {
id : "asset-created" ,
} ) ;
2026-03-18 21:54:10 -05:00
accessSvc . listActiveUserMemberships . mockResolvedValue ( [
{
id : "membership-1" ,
companyId : "company-1" ,
principalType : "user" ,
principalId : "user-1" ,
membershipRole : "owner" ,
status : "active" ,
} ,
] ) ;
accessSvc . copyActiveUserMemberships . mockResolvedValue ( [ ] ) ;
2026-03-17 13:42:00 -05:00
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 < string , unknown > } ) = > ( {
bundle : null ,
adapterConfig : {
. . . agent . adapterConfig ,
instructionsBundleMode : "managed" ,
instructionsRootPath : ` /tmp/ ${ agent . id } ` ,
instructionsEntryFile : "AGENTS.md" ,
instructionsFilePath : ` /tmp/ ${ agent . id } /AGENTS.md ` ,
} ,
} ) ) ;
2026-03-14 09:46:16 -05:00
} ) ;
2026-03-14 18:59:26 -05:00
it ( "exports referenced skills as stubs by default with sanitized Paperclip extension data" , async ( ) = > {
2026-03-14 09:46:16 -05:00
const portability = companyPortabilityService ( { } as any ) ;
const exported = await portability . exportBundle ( "company-1" , {
include : {
company : true ,
agents : true ,
projects : false ,
issues : false ,
} ,
} ) ;
2026-03-19 07:28:26 -05:00
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"' ) ;
2026-03-18 16:54:25 -05:00
expect ( exported . files [ "skills/paperclipai/paperclip/paperclip/references/api.md" ] ) . toBeUndefined ( ) ;
2026-03-19 07:28:26 -05:00
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" ) ;
2026-03-14 09:46:16 -05:00
2026-03-19 07:28:26 -05:00
const extension = asTextFile ( exported . files [ ".paperclip.yaml" ] ) ;
2026-03-14 09:46:16 -05:00
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: ""' ) ;
2026-03-16 08:55:37 -05:00
expect ( extension ) . not . toContain ( "paperclipSkillSync" ) ;
2026-03-14 09:46:16 -05:00
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." ) ;
} ) ;
2026-03-14 18:59:26 -05:00
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 ,
} ) ;
2026-03-19 07:28:26 -05:00
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" ) ;
2026-03-18 16:23:19 -05:00
} ) ;
2026-03-19 16:29:11 -05:00
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 ( ) ;
} ) ;
2026-03-19 07:24:04 -05:00
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"' ) ;
} ) ;
2026-03-18 16:54:25 -05:00
it ( "exports duplicate skill slugs into readable namespaced paths" , async ( ) = > {
2026-03-18 16:23:19 -05:00
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 ,
} ,
} ) ;
2026-03-19 07:28:26 -05:00
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" ) ;
2026-03-14 18:59:26 -05:00
} ) ;
2026-03-18 21:54:10 -05:00
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 ) ;
} ) ;
2026-03-14 09:46:16 -05:00
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" ,
} ,
] ) ;
} ) ;
2026-03-14 18:59:26 -05:00
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" ) ;
2026-03-20 06:20:30 -05:00
const textOnlyFiles = Object . fromEntries ( Object . entries ( exported . files ) . filter ( ( [ , v ] ) = > typeof v === "string" ) ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith ( "company-imported" , textOnlyFiles , {
2026-03-18 21:54:10 -05:00
onConflict : "replace" ,
} ) ;
2026-03-14 18:59:26 -05:00
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterConfig : expect.objectContaining ( {
paperclipSkillSync : {
2026-03-16 18:27:20 -05:00
desiredSkills : [ paperclipKey ] ,
2026-03-14 18:59:26 -05:00
} ,
} ) ,
} ) ) ;
} ) ;
2026-03-16 10:14:09 -05:00
2026-03-19 07:24:04 -05:00
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" ,
} ) ;
} ) ;
2026-03-18 21:54:10 -05:00
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" ) ;
2026-03-20 06:20:30 -05:00
const textOnlyFiles = Object . fromEntries ( Object . entries ( exported . files ) . filter ( ( [ , v ] ) = > typeof v === "string" ) ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith ( "company-imported" , textOnlyFiles , {
2026-03-18 21:54:10 -05:00
onConflict : "rename" ,
} ) ;
} ) ;
2026-03-16 10:14:09 -05:00
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 ) ,
} ) ,
2026-03-18 21:54:10 -05:00
{
onConflict : "replace" ,
} ,
2026-03-16 10:14:09 -05:00
) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith (
"company-1" ,
expect . not . objectContaining ( {
"agents/claudecoder/AGENTS.md" : expect . any ( String ) ,
} ) ,
2026-03-18 21:54:10 -05:00
{
onConflict : "replace" ,
} ,
2026-03-16 10:14:09 -05:00
) ;
expect ( agentSvc . create ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-1" , expect . objectContaining ( {
name : "CMO" ,
} ) ) ;
expect ( result . company . action ) . toBe ( "unchanged" ) ;
expect ( result . agents ) . toEqual ( [
{
slug : "cmo" ,
id : "agent-cmo" ,
action : "created" ,
name : "CMO" ,
reason : null ,
} ,
] ) ;
} ) ;
2026-03-16 12:17:28 -05:00
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 ( ) ,
2026-03-17 13:42:00 -05:00
promptTemplate : expect.anything ( ) ,
2026-03-16 12:17:28 -05:00
} ) ,
} ) ) ;
2026-03-17 13:42:00 -05:00
expect ( agentInstructionsSvc . materializeManagedBundle ) . toHaveBeenCalledWith (
expect . objectContaining ( { name : "ClaudeCoder" } ) ,
expect . objectContaining ( {
"AGENTS.md" : expect . stringContaining ( "You are ClaudeCoder." ) ,
} ) ,
expect . objectContaining ( {
clearLegacyPromptTemplate : true ,
replaceExisting : true ,
} ) ,
) ;
2026-03-16 12:17:28 -05:00
} ) ;
2026-03-14 09:46:16 -05:00
} ) ;