2026-03-13 11:53:56 -05:00
import { describe , expect , it } from "vitest" ;
import fs from "node:fs/promises" ;
import os from "node:os" ;
import path from "node:path" ;
import { execute } from "@paperclipai/adapter-codex-local/server" ;
async function writeFakeCodexCommand ( commandPath : string ) : Promise < void > {
const script = ` #!/usr/bin/env node
const fs = require ( "node:fs" ) ;
const capturePath = process . env . PAPERCLIP_TEST_CAPTURE_PATH ;
const payload = {
argv : process.argv.slice ( 2 ) ,
prompt : fs.readFileSync ( 0 , "utf8" ) ,
codexHome : process.env.CODEX_HOME || null ,
paperclipEnvKeys : Object.keys ( process . env )
. filter ( ( key ) = > key . startsWith ( "PAPERCLIP_" ) )
. sort ( ) ,
} ;
if ( capturePath ) {
fs . writeFileSync ( capturePath , JSON . stringify ( payload ) , "utf8" ) ;
}
console . log ( JSON . stringify ( { type : "thread.started" , thread_id : "codex-session-1" } ) ) ;
console . log ( JSON . stringify ( { type : "item.completed" , item : { type : "agent_message" , text : "hello" } } ) ) ;
console . log ( JSON . stringify ( { type : "turn.completed" , usage : { input_tokens : 1 , cached_input_tokens : 0 , output_tokens : 1 } } ) ) ;
` ;
await fs . writeFile ( commandPath , script , "utf8" ) ;
await fs . chmod ( commandPath , 0 o755 ) ;
}
type CapturePayload = {
argv : string [ ] ;
prompt : string ;
codexHome : string | null ;
paperclipEnvKeys : string [ ] ;
} ;
2026-03-16 18:09:43 -05:00
type LogEntry = {
stream : "stdout" | "stderr" ;
chunk : string ;
} ;
2026-03-13 11:53:56 -05:00
describe ( "codex execute" , ( ) = > {
2026-03-20 14:44:27 -05:00
it ( "uses a Paperclip-managed CODEX_HOME outside worktree mode while preserving shared auth and config" , async ( ) = > {
const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-execute-default-" ) ) ;
const workspace = path . join ( root , "workspace" ) ;
const commandPath = path . join ( root , "codex" ) ;
const capturePath = path . join ( root , "capture.json" ) ;
const sharedCodexHome = path . join ( root , "shared-codex-home" ) ;
const paperclipHome = path . join ( root , "paperclip-home" ) ;
const managedCodexHome = path . join (
paperclipHome ,
"instances" ,
"default" ,
"companies" ,
"company-1" ,
"codex-home" ,
) ;
await fs . mkdir ( workspace , { recursive : true } ) ;
await fs . mkdir ( sharedCodexHome , { recursive : true } ) ;
await fs . writeFile ( path . join ( sharedCodexHome , "auth.json" ) , '{"token":"shared"}\n' , "utf8" ) ;
await fs . writeFile ( path . join ( sharedCodexHome , "config.toml" ) , 'model = "codex-mini-latest"\n' , "utf8" ) ;
await writeFakeCodexCommand ( commandPath ) ;
const previousHome = process . env . HOME ;
const previousPaperclipHome = process . env . PAPERCLIP_HOME ;
const previousPaperclipInstanceId = process . env . PAPERCLIP_INSTANCE_ID ;
const previousPaperclipInWorktree = process . env . PAPERCLIP_IN_WORKTREE ;
const previousCodexHome = process . env . CODEX_HOME ;
process . env . HOME = root ;
process . env . PAPERCLIP_HOME = paperclipHome ;
delete process . env . PAPERCLIP_INSTANCE_ID ;
delete process . env . PAPERCLIP_IN_WORKTREE ;
process . env . CODEX_HOME = sharedCodexHome ;
try {
const logs : LogEntry [ ] = [ ] ;
const result = await execute ( {
runId : "run-default" ,
agent : {
id : "agent-1" ,
companyId : "company-1" ,
name : "Codex Coder" ,
adapterType : "codex_local" ,
adapterConfig : { } ,
} ,
runtime : {
sessionId : null ,
sessionParams : null ,
sessionDisplayId : null ,
taskKey : null ,
} ,
config : {
command : commandPath ,
cwd : workspace ,
env : {
PAPERCLIP_TEST_CAPTURE_PATH : capturePath ,
} ,
promptTemplate : "Follow the paperclip heartbeat." ,
} ,
context : { } ,
authToken : "run-jwt-token" ,
onLog : async ( stream , chunk ) = > {
logs . push ( { stream , chunk } ) ;
} ,
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . errorMessage ) . toBeNull ( ) ;
const capture = JSON . parse ( await fs . readFile ( capturePath , "utf8" ) ) as CapturePayload ;
expect ( capture . codexHome ) . toBe ( managedCodexHome ) ;
const managedAuth = path . join ( managedCodexHome , "auth.json" ) ;
const managedConfig = path . join ( managedCodexHome , "config.toml" ) ;
expect ( ( await fs . lstat ( managedAuth ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
expect ( await fs . realpath ( managedAuth ) ) . toBe ( await fs . realpath ( path . join ( sharedCodexHome , "auth.json" ) ) ) ;
expect ( ( await fs . lstat ( managedConfig ) ) . isFile ( ) ) . toBe ( true ) ;
expect ( await fs . readFile ( managedConfig , "utf8" ) ) . toBe ( 'model = "codex-mini-latest"\n' ) ;
await expect ( fs . lstat ( path . join ( sharedCodexHome , "companies" , "company-1" ) ) ) . rejects . toThrow ( ) ;
expect ( logs ) . toContainEqual (
expect . objectContaining ( {
stream : "stdout" ,
chunk : expect.stringContaining ( "Using Paperclip-managed Codex home" ) ,
} ) ,
) ;
} finally {
if ( previousHome === undefined ) delete process . env . HOME ;
else process . env . HOME = previousHome ;
if ( previousPaperclipHome === undefined ) delete process . env . PAPERCLIP_HOME ;
else process . env . PAPERCLIP_HOME = previousPaperclipHome ;
if ( previousPaperclipInstanceId === undefined ) delete process . env . PAPERCLIP_INSTANCE_ID ;
else process . env . PAPERCLIP_INSTANCE_ID = previousPaperclipInstanceId ;
if ( previousPaperclipInWorktree === undefined ) delete process . env . PAPERCLIP_IN_WORKTREE ;
else process . env . PAPERCLIP_IN_WORKTREE = previousPaperclipInWorktree ;
if ( previousCodexHome === undefined ) delete process . env . CODEX_HOME ;
else process . env . CODEX_HOME = previousCodexHome ;
await fs . rm ( root , { recursive : true , force : true } ) ;
}
} ) ;
2026-03-23 16:55:10 -05:00
it ( "emits a command note that Codex auto-applies repo-scoped AGENTS.md files" , async ( ) = > {
const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-execute-notes-" ) ) ;
const workspace = path . join ( root , "workspace" ) ;
const commandPath = path . join ( root , "codex" ) ;
const capturePath = path . join ( root , "capture.json" ) ;
await fs . mkdir ( workspace , { recursive : true } ) ;
await writeFakeCodexCommand ( commandPath ) ;
const previousHome = process . env . HOME ;
process . env . HOME = root ;
let commandNotes : string [ ] = [ ] ;
try {
const result = await execute ( {
runId : "run-notes" ,
agent : {
id : "agent-1" ,
companyId : "company-1" ,
name : "Codex Coder" ,
adapterType : "codex_local" ,
adapterConfig : { } ,
} ,
runtime : {
sessionId : null ,
sessionParams : null ,
sessionDisplayId : null ,
taskKey : null ,
} ,
config : {
command : commandPath ,
cwd : workspace ,
env : {
PAPERCLIP_TEST_CAPTURE_PATH : capturePath ,
} ,
promptTemplate : "Follow the paperclip heartbeat." ,
} ,
context : { } ,
authToken : "run-jwt-token" ,
onLog : async ( ) = > { } ,
onMeta : async ( meta ) = > {
commandNotes = Array . isArray ( meta . commandNotes ) ? meta . commandNotes : [ ] ;
} ,
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . errorMessage ) . toBeNull ( ) ;
expect ( commandNotes ) . toContain (
"Codex exec automatically applies repo-scoped AGENTS.md instructions from the current workspace; Paperclip does not currently suppress that discovery." ,
) ;
} finally {
if ( previousHome === undefined ) delete process . env . HOME ;
else process . env . HOME = previousHome ;
await fs . rm ( root , { recursive : true , force : true } ) ;
}
} ) ;
2026-03-28 15:42:14 -05:00
it ( "logs HOME and the resolved executable path in invocation metadata" , async ( ) = > {
const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-execute-meta-" ) ) ;
const workspace = path . join ( root , "workspace" ) ;
const binDir = path . join ( root , "bin" ) ;
const commandPath = path . join ( binDir , "codex" ) ;
const capturePath = path . join ( root , "capture.json" ) ;
await fs . mkdir ( workspace , { recursive : true } ) ;
await fs . mkdir ( binDir , { recursive : true } ) ;
await writeFakeCodexCommand ( commandPath ) ;
const previousHome = process . env . HOME ;
const previousPath = process . env . PATH ;
process . env . HOME = root ;
process . env . PATH = ` ${ binDir } ${ path . delimiter } ${ process . env . PATH ? ? "" } ` ;
let loggedCommand : string | null = null ;
let loggedEnv : Record < string , string > = { } ;
try {
const result = await execute ( {
runId : "run-meta" ,
agent : {
id : "agent-1" ,
companyId : "company-1" ,
name : "Codex Coder" ,
adapterType : "codex_local" ,
adapterConfig : { } ,
} ,
runtime : {
sessionId : null ,
sessionParams : null ,
sessionDisplayId : null ,
taskKey : null ,
} ,
config : {
command : "codex" ,
cwd : workspace ,
env : {
PAPERCLIP_TEST_CAPTURE_PATH : capturePath ,
} ,
promptTemplate : "Follow the paperclip heartbeat." ,
} ,
context : { } ,
authToken : "run-jwt-token" ,
onLog : async ( ) = > { } ,
onMeta : async ( meta ) = > {
loggedCommand = meta . command ;
loggedEnv = meta . env ? ? { } ;
} ,
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . errorMessage ) . toBeNull ( ) ;
expect ( loggedCommand ) . toBe ( commandPath ) ;
expect ( loggedEnv . HOME ) . toBe ( root ) ;
expect ( loggedEnv . PAPERCLIP_RESOLVED_COMMAND ) . toBe ( commandPath ) ;
} finally {
if ( previousHome === undefined ) delete process . env . HOME ;
else process . env . HOME = previousHome ;
if ( previousPath === undefined ) delete process . env . PATH ;
else process . env . PATH = previousPath ;
await fs . rm ( root , { recursive : true , force : true } ) ;
}
} ) ;
2026-03-13 11:53:56 -05:00
it ( "uses a worktree-isolated CODEX_HOME while preserving shared auth and config" , async ( ) = > {
const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-execute-" ) ) ;
const workspace = path . join ( root , "workspace" ) ;
const commandPath = path . join ( root , "codex" ) ;
const capturePath = path . join ( root , "capture.json" ) ;
const sharedCodexHome = path . join ( root , "shared-codex-home" ) ;
const paperclipHome = path . join ( root , "paperclip-home" ) ;
2026-03-18 14:38:39 -05:00
const isolatedCodexHome = path . join (
paperclipHome ,
"instances" ,
"worktree-1" ,
"companies" ,
"company-1" ,
"codex-home" ,
) ;
2026-03-25 16:04:53 -07:00
const homeSkill = path . join ( isolatedCodexHome , "skills" , "paperclip" ) ;
2026-03-13 11:53:56 -05:00
await fs . mkdir ( workspace , { recursive : true } ) ;
await fs . mkdir ( sharedCodexHome , { recursive : true } ) ;
await fs . writeFile ( path . join ( sharedCodexHome , "auth.json" ) , '{"token":"shared"}\n' , "utf8" ) ;
await fs . writeFile ( path . join ( sharedCodexHome , "config.toml" ) , 'model = "codex-mini-latest"\n' , "utf8" ) ;
await writeFakeCodexCommand ( commandPath ) ;
const previousHome = process . env . HOME ;
const previousPaperclipHome = process . env . PAPERCLIP_HOME ;
const previousPaperclipInstanceId = process . env . PAPERCLIP_INSTANCE_ID ;
const previousPaperclipInWorktree = process . env . PAPERCLIP_IN_WORKTREE ;
const previousCodexHome = process . env . CODEX_HOME ;
process . env . HOME = root ;
process . env . PAPERCLIP_HOME = paperclipHome ;
process . env . PAPERCLIP_INSTANCE_ID = "worktree-1" ;
process . env . PAPERCLIP_IN_WORKTREE = "true" ;
process . env . CODEX_HOME = sharedCodexHome ;
try {
2026-03-16 18:09:43 -05:00
const logs : LogEntry [ ] = [ ] ;
2026-03-13 11:53:56 -05:00
const result = await execute ( {
runId : "run-1" ,
agent : {
id : "agent-1" ,
companyId : "company-1" ,
name : "Codex Coder" ,
adapterType : "codex_local" ,
adapterConfig : { } ,
} ,
runtime : {
sessionId : null ,
sessionParams : null ,
sessionDisplayId : null ,
taskKey : null ,
} ,
config : {
command : commandPath ,
cwd : workspace ,
env : {
PAPERCLIP_TEST_CAPTURE_PATH : capturePath ,
} ,
promptTemplate : "Follow the paperclip heartbeat." ,
} ,
context : { } ,
authToken : "run-jwt-token" ,
2026-03-16 18:09:43 -05:00
onLog : async ( stream , chunk ) = > {
logs . push ( { stream , chunk } ) ;
} ,
2026-03-13 11:53:56 -05:00
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . errorMessage ) . toBeNull ( ) ;
const capture = JSON . parse ( await fs . readFile ( capturePath , "utf8" ) ) as CapturePayload ;
expect ( capture . codexHome ) . toBe ( isolatedCodexHome ) ;
expect ( capture . argv ) . toEqual ( expect . arrayContaining ( [ "exec" , "--json" , "-" ] ) ) ;
expect ( capture . prompt ) . toContain ( "Follow the paperclip heartbeat." ) ;
expect ( capture . paperclipEnvKeys ) . toEqual (
expect . arrayContaining ( [
"PAPERCLIP_AGENT_ID" ,
"PAPERCLIP_API_KEY" ,
"PAPERCLIP_API_URL" ,
"PAPERCLIP_COMPANY_ID" ,
"PAPERCLIP_RUN_ID" ,
] ) ,
) ;
const isolatedAuth = path . join ( isolatedCodexHome , "auth.json" ) ;
const isolatedConfig = path . join ( isolatedCodexHome , "config.toml" ) ;
expect ( ( await fs . lstat ( isolatedAuth ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
expect ( await fs . realpath ( isolatedAuth ) ) . toBe ( await fs . realpath ( path . join ( sharedCodexHome , "auth.json" ) ) ) ;
expect ( ( await fs . lstat ( isolatedConfig ) ) . isFile ( ) ) . toBe ( true ) ;
expect ( await fs . readFile ( isolatedConfig , "utf8" ) ) . toBe ( 'model = "codex-mini-latest"\n' ) ;
2026-03-25 15:55:51 -07:00
expect ( ( await fs . lstat ( homeSkill ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
2026-03-16 18:09:43 -05:00
expect ( logs ) . toContainEqual (
expect . objectContaining ( {
stream : "stdout" ,
chunk : expect.stringContaining ( "Using worktree-isolated Codex home" ) ,
} ) ,
) ;
expect ( logs ) . toContainEqual (
expect . objectContaining ( {
stream : "stdout" ,
chunk : expect.stringContaining ( 'Injected Codex skill "paperclip"' ) ,
} ) ,
) ;
2026-03-13 11:53:56 -05:00
} finally {
if ( previousHome === undefined ) delete process . env . HOME ;
else process . env . HOME = previousHome ;
if ( previousPaperclipHome === undefined ) delete process . env . PAPERCLIP_HOME ;
else process . env . PAPERCLIP_HOME = previousPaperclipHome ;
if ( previousPaperclipInstanceId === undefined ) delete process . env . PAPERCLIP_INSTANCE_ID ;
else process . env . PAPERCLIP_INSTANCE_ID = previousPaperclipInstanceId ;
if ( previousPaperclipInWorktree === undefined ) delete process . env . PAPERCLIP_IN_WORKTREE ;
else process . env . PAPERCLIP_IN_WORKTREE = previousPaperclipInWorktree ;
if ( previousCodexHome === undefined ) delete process . env . CODEX_HOME ;
else process . env . CODEX_HOME = previousCodexHome ;
await fs . rm ( root , { recursive : true , force : true } ) ;
}
} ) ;
it ( "respects an explicit CODEX_HOME config override even in worktree mode" , async ( ) = > {
const root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "paperclip-codex-execute-explicit-" ) ) ;
const workspace = path . join ( root , "workspace" ) ;
const commandPath = path . join ( root , "codex" ) ;
const capturePath = path . join ( root , "capture.json" ) ;
const sharedCodexHome = path . join ( root , "shared-codex-home" ) ;
const explicitCodexHome = path . join ( root , "explicit-codex-home" ) ;
const paperclipHome = path . join ( root , "paperclip-home" ) ;
await fs . mkdir ( workspace , { recursive : true } ) ;
await fs . mkdir ( sharedCodexHome , { recursive : true } ) ;
await fs . writeFile ( path . join ( sharedCodexHome , "auth.json" ) , '{"token":"shared"}\n' , "utf8" ) ;
await writeFakeCodexCommand ( commandPath ) ;
const previousHome = process . env . HOME ;
const previousPaperclipHome = process . env . PAPERCLIP_HOME ;
const previousPaperclipInstanceId = process . env . PAPERCLIP_INSTANCE_ID ;
const previousPaperclipInWorktree = process . env . PAPERCLIP_IN_WORKTREE ;
const previousCodexHome = process . env . CODEX_HOME ;
process . env . HOME = root ;
process . env . PAPERCLIP_HOME = paperclipHome ;
process . env . PAPERCLIP_INSTANCE_ID = "worktree-1" ;
process . env . PAPERCLIP_IN_WORKTREE = "true" ;
process . env . CODEX_HOME = sharedCodexHome ;
try {
const result = await execute ( {
runId : "run-2" ,
agent : {
id : "agent-1" ,
companyId : "company-1" ,
name : "Codex Coder" ,
adapterType : "codex_local" ,
adapterConfig : { } ,
} ,
runtime : {
sessionId : null ,
sessionParams : null ,
sessionDisplayId : null ,
taskKey : null ,
} ,
config : {
command : commandPath ,
cwd : workspace ,
env : {
PAPERCLIP_TEST_CAPTURE_PATH : capturePath ,
CODEX_HOME : explicitCodexHome ,
} ,
promptTemplate : "Follow the paperclip heartbeat." ,
} ,
context : { } ,
authToken : "run-jwt-token" ,
onLog : async ( ) = > { } ,
} ) ;
expect ( result . exitCode ) . toBe ( 0 ) ;
expect ( result . errorMessage ) . toBeNull ( ) ;
const capture = JSON . parse ( await fs . readFile ( capturePath , "utf8" ) ) as CapturePayload ;
expect ( capture . codexHome ) . toBe ( explicitCodexHome ) ;
2026-03-25 16:04:53 -07:00
expect ( ( await fs . lstat ( path . join ( explicitCodexHome , "skills" , "paperclip" ) ) ) . isSymbolicLink ( ) ) . toBe ( true ) ;
2026-03-13 11:53:56 -05:00
await expect ( fs . lstat ( path . join ( paperclipHome , "instances" , "worktree-1" , "codex-home" ) ) ) . rejects . toThrow ( ) ;
} finally {
if ( previousHome === undefined ) delete process . env . HOME ;
else process . env . HOME = previousHome ;
if ( previousPaperclipHome === undefined ) delete process . env . PAPERCLIP_HOME ;
else process . env . PAPERCLIP_HOME = previousPaperclipHome ;
if ( previousPaperclipInstanceId === undefined ) delete process . env . PAPERCLIP_INSTANCE_ID ;
else process . env . PAPERCLIP_INSTANCE_ID = previousPaperclipInstanceId ;
if ( previousPaperclipInWorktree === undefined ) delete process . env . PAPERCLIP_IN_WORKTREE ;
else process . env . PAPERCLIP_IN_WORKTREE = previousPaperclipInWorktree ;
if ( previousCodexHome === undefined ) delete process . env . CODEX_HOME ;
else process . env . CODEX_HOME = previousCodexHome ;
await fs . rm ( root , { recursive : true , force : true } ) ;
}
} ) ;
} ) ;