2026-03-14 09:46:16 -05:00
import { beforeEach , describe , expect , it , vi } from "vitest" ;
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 ( ) ,
} ;
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 ( ) ,
readFile : vi.fn ( ) ,
importPackageFiles : 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-14 09:46:16 -05:00
const { companyPortabilityService } = await import ( "../services/company-portability.js" ) ;
describe ( "company portability" , ( ) = > {
beforeEach ( ( ) = > {
vi . clearAllMocks ( ) ;
companySvc . getById . mockResolvedValue ( {
id : "company-1" ,
name : "Paperclip" ,
description : null ,
brandColor : "#5c5fff" ,
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 : {
desiredSkills : [ "paperclip" ] ,
} ,
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-14 18:59:26 -05:00
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 ( [ ] ) ;
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 ,
} ,
} ) ;
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." ) ;
2026-03-14 18:59:26 -05:00
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" ) ;
2026-03-14 09:46:16 -05:00
const extension = 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: ""' ) ;
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 ,
} ) ;
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" ) ;
} ) ;
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" ) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith ( "company-imported" , exported . files ) ;
expect ( agentSvc . create ) . toHaveBeenCalledWith ( "company-imported" , expect . objectContaining ( {
adapterConfig : expect.objectContaining ( {
paperclipSkillSync : {
desiredSkills : [ "paperclip" ] ,
} ,
} ) ,
} ) ) ;
} ) ;
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 ) ,
} ) ,
) ;
expect ( companySkillSvc . importPackageFiles ) . toHaveBeenCalledWith (
"company-1" ,
expect . not . objectContaining ( {
"agents/claudecoder/AGENTS.md" : expect . any ( String ) ,
} ) ,
) ;
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-14 09:46:16 -05:00
} ) ;