229 lines
9.2 KiB
TypeScript
229 lines
9.2 KiB
TypeScript
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 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", "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;
|
|
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");
|
|
const isolatedSkill = path.join(isolatedCodexHome, "skills", "paperclip");
|
|
|
|
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(isolatedSkill)).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);
|
|
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 });
|
|
}
|
|
});
|
|
});
|