Spaces:
Paused
Paused
| 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, | |
| 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<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 }); | |
| } | |
| }); | |
| 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<string, number> = {}; | |
| 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 }); | |
| } | |
| }); | |
| }); | |