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 { 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, paperclipWakePayloadJson: process.env.PAPERCLIP_WAKE_PAYLOAD_JSON || 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; paperclipWakePayloadJson: 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("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 = {}; 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 }); } }); it("injects structured Paperclip wake payloads into env and prompt", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-wake-")); 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; try { const result = await execute({ runId: "run-wake", 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: { issueId: "issue-1", taskId: "issue-1", wakeReason: "issue_commented", wakeCommentId: "comment-2", paperclipWake: { reason: "issue_commented", issue: { id: "issue-1", identifier: "PAP-874", title: "chat-speed issues", status: "in_progress", priority: "medium", }, commentIds: ["comment-1", "comment-2"], latestCommentId: "comment-2", comments: [ { id: "comment-1", issueId: "issue-1", body: "First comment", bodyTruncated: false, createdAt: "2026-03-28T14:35:00.000Z", author: { type: "user", id: "user-1" }, }, { id: "comment-2", issueId: "issue-1", body: "Second comment", bodyTruncated: false, createdAt: "2026-03-28T14:35:10.000Z", author: { type: "user", id: "user-1" }, }, ], commentWindow: { requestedCount: 2, includedCount: 2, missingCount: 0, }, truncated: false, fallbackFetchNeeded: false, }, }, 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.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON"); expect(capture.paperclipWakePayloadJson).not.toBeNull(); expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({ reason: "issue_commented", latestCommentId: "comment-2", commentIds: ["comment-1", "comment-2"], }); expect(capture.prompt).toContain("## Paperclip Wake Payload"); expect(capture.prompt).toContain("Treat this wake payload as the highest-priority change for the current heartbeat."); expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake."); expect(capture.prompt).toContain( "acknowledge the latest comment and explain how it changes your next action.", ); expect(capture.prompt).toContain("First comment"); expect(capture.prompt).toContain("Second comment"); } finally { if (previousHome === undefined) delete process.env.HOME; else process.env.HOME = previousHome; await fs.rm(root, { recursive: true, force: true }); } }); it("uses a compact wake delta instead of the full heartbeat prompt when resuming a session", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-resume-wake-")); const workspace = path.join(root, "workspace"); const commandPath = path.join(root, "codex"); const capturePath = path.join(root, "capture.json"); const instructionsPath = path.join(root, "AGENTS.md"); await fs.mkdir(workspace, { recursive: true }); await fs.writeFile(instructionsPath, "You are managed instructions.\n", "utf8"); await writeFakeCodexCommand(commandPath); const previousHome = process.env.HOME; process.env.HOME = root; let invocationPrompt = ""; let invocationNotes: string[] = []; let promptMetrics: Record = {}; try { const result = await execute({ runId: "run-resume-wake", agent: { id: "agent-1", companyId: "company-1", name: "Codex Coder", adapterType: "codex_local", adapterConfig: {}, }, runtime: { sessionId: null, sessionParams: { sessionId: "codex-session-1", cwd: workspace, }, sessionDisplayId: null, taskKey: null, }, config: { command: commandPath, cwd: workspace, instructionsFilePath: instructionsPath, env: { PAPERCLIP_TEST_CAPTURE_PATH: capturePath, }, promptTemplate: "Follow the paperclip heartbeat.", }, context: { issueId: "issue-1", taskId: "issue-1", wakeReason: "issue_commented", wakeCommentId: "comment-2", paperclipWake: { reason: "issue_commented", issue: { id: "issue-1", identifier: "PAP-874", title: "chat-speed issues", status: "in_progress", priority: "medium", }, commentIds: ["comment-2"], latestCommentId: "comment-2", comments: [ { id: "comment-2", issueId: "issue-1", body: "Second comment", bodyTruncated: false, createdAt: "2026-03-28T14:35:10.000Z", author: { type: "user", id: "user-1" }, }, ], commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0, }, truncated: false, fallbackFetchNeeded: false, }, }, authToken: "run-jwt-token", onLog: async () => {}, onMeta: async (meta) => { invocationPrompt = meta.prompt ?? ""; invocationNotes = meta.commandNotes ?? []; promptMetrics = meta.promptMetrics ?? {}; }, }); expect(result.exitCode).toBe(0); expect(result.errorMessage).toBeNull(); const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload; expect(capture.argv).toEqual(expect.arrayContaining(["resume", "codex-session-1", "-"])); expect(capture.prompt).toContain("## Paperclip Resume Delta"); expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake."); expect(capture.prompt).toContain("Second comment"); expect(capture.prompt).not.toContain("Follow the paperclip heartbeat."); expect(capture.prompt).not.toContain("You are managed instructions."); expect(invocationPrompt).toContain("## Paperclip Resume Delta"); expect(invocationNotes).toContain( "Skipped stdin instruction reinjection because an existing Codex session is being resumed with a wake delta.", ); expect(promptMetrics.instructionsChars).toBe(0); expect(promptMetrics.heartbeatPromptChars).toBe(0); } 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 }); } }); });