| 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, 0o755); |
| } |
|
|
| type CapturePayload = { |
| argv: string[]; |
| prompt: string; |
| codexHome: string | null; |
| paperclipEnvKeys: string[]; |
| }; |
|
|
| type LogEntry = { |
| stream: "stdout" | "stderr"; |
| chunk: string; |
| }; |
|
|
| describe("codex execute", () => { |
| 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 }); |
| } |
| }); |
|
|
| 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 }); |
| } |
| }); |
|
|
| 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"); |
| const isolatedCodexHome = path.join( |
| paperclipHome, |
| "instances", |
| "worktree-1", |
| "companies", |
| "company-1", |
| "codex-home", |
| ); |
| const homeSkill = path.join(isolatedCodexHome, "skills", "paperclip"); |
| 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 { |
| const logs: LogEntry[] = []; |
| 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", |
| 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(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'); |
| expect((await fs.lstat(homeSkill)).isSymbolicLink()).toBe(true); |
| 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"'), |
| }), |
| ); |
| } 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); |
| expect((await fs.lstat(path.join(explicitCodexHome, "skills", "paperclip"))).isSymbolicLink()).toBe(true); |
| 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 }); |
| } |
| }); |
| }); |
|
|