import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; import { afterEach, describe, expect, it } from "vitest"; import { prepareCommandManagedRuntime } from "./command-managed-runtime.js"; import type { RunProcessResult } from "./server-utils.js"; const execFile = promisify(execFileCallback); describe("command managed runtime", () => { const cleanupDirs: string[] = []; afterEach(async () => { while (cleanupDirs.length > 0) { const dir = cleanupDirs.pop(); if (!dir) continue; await rm(dir, { recursive: true, force: true }).catch(() => undefined); } }); it("keeps the runtime overlay out of sandbox workspace sync by default", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-")); cleanupDirs.push(rootDir); const localWorkspaceDir = path.join(rootDir, "local-workspace"); const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); await mkdir(path.join(localWorkspaceDir, ".paperclip-runtime"), { recursive: true }); await mkdir(remoteWorkspaceDir, { recursive: true }); await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8"); await writeFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "{\"keep\":true}\n", "utf8"); const calls: Array<{ command: string; args?: string[]; cwd?: string; env?: Record; stdin?: string; timeoutMs?: number; }> = []; const runner = { execute: async (input: { command: string; args?: string[]; cwd?: string; env?: Record; stdin?: string; timeoutMs?: number; }): Promise => { calls.push({ ...input }); const startedAt = new Date().toISOString(); const env = { ...process.env, ...input.env, }; const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command; const args = [...(input.args ?? [])]; if ( input.stdin != null && (input.command === "sh" || input.command === "bash") && (args[0] === "-c" || args[0] === "-lc") && typeof args[1] === "string" ) { env.PAPERCLIP_TEST_STDIN = input.stdin; args[1] = `printf '%s' \"$PAPERCLIP_TEST_STDIN\" | (${args[1]})`; } try { const result = await execFile(command, args, { cwd: input.cwd, env, maxBuffer: 32 * 1024 * 1024, timeout: input.timeoutMs, }); return { exitCode: 0, signal: null, timedOut: false, stdout: result.stdout, stderr: result.stderr, pid: null, startedAt, }; } catch (error) { const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number | null; signal?: NodeJS.Signals | null; killed?: boolean; }; return { exitCode: typeof err.code === "number" ? err.code : null, signal: err.signal ?? null, timedOut: Boolean(err.killed && input.timeoutMs), stdout: err.stdout ?? "", stderr: err.stderr ?? "", pid: null, startedAt, }; } }, }; const prepared = await prepareCommandManagedRuntime({ runner, spec: { remoteCwd: remoteWorkspaceDir, timeoutMs: 30_000, }, adapterKey: "claude", workspaceLocalDir: localWorkspaceDir, }); await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n"); await expect(readFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).rejects .toMatchObject({ code: "ENOENT" }); expect(calls.every((call) => call.stdin == null)).toBe(true); await mkdir(path.join(remoteWorkspaceDir, ".paperclip-runtime"), { recursive: true }); await writeFile(path.join(remoteWorkspaceDir, "README.md"), "remote workspace\n", "utf8"); await writeFile(path.join(remoteWorkspaceDir, ".paperclip-runtime", "remote-state.json"), "{\"remote\":true}\n", "utf8"); await prepared.restoreWorkspace(); await expect(readFile(path.join(localWorkspaceDir, "README.md"), "utf8")).resolves.toBe("remote workspace\n"); await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "state.json"), "utf8")).resolves .toBe("{\"keep\":true}\n"); await expect(readFile(path.join(localWorkspaceDir, ".paperclip-runtime", "remote-state.json"), "utf8")).rejects .toMatchObject({ code: "ENOENT" }); expect(calls.every((call) => call.stdin == null)).toBe(true); }); it("runs setup commands from the existing sandbox cwd when staging into a nested remote workspace dir", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-command-runtime-nested-")); cleanupDirs.push(rootDir); const localWorkspaceDir = path.join(rootDir, "local-workspace"); const remoteBaseDir = path.join(rootDir, "remote-base"); const remoteWorkspaceDir = path.join(remoteBaseDir, ".paperclip-runtime", "runs", "test", "workspace"); await mkdir(localWorkspaceDir, { recursive: true }); await mkdir(remoteBaseDir, { recursive: true }); await writeFile(path.join(localWorkspaceDir, "README.md"), "local workspace\n", "utf8"); const calls: Array<{ command: string; args?: string[]; cwd?: string; env?: Record; stdin?: string; timeoutMs?: number; }> = []; const runner = { execute: async (input: { command: string; args?: string[]; cwd?: string; env?: Record; stdin?: string; timeoutMs?: number; }): Promise => { calls.push({ ...input }); const startedAt = new Date().toISOString(); try { const result = await execFile(input.command === "sh" ? "/bin/sh" : input.command, input.args ?? [], { cwd: input.cwd, env: { ...process.env, ...input.env, }, maxBuffer: 32 * 1024 * 1024, timeout: input.timeoutMs, }); return { exitCode: 0, signal: null, timedOut: false, stdout: result.stdout, stderr: result.stderr, pid: null, startedAt, }; } catch (error) { const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; code?: string | number | null; signal?: NodeJS.Signals | null; killed?: boolean; }; return { exitCode: typeof err.code === "number" ? err.code : null, signal: err.signal ?? null, timedOut: Boolean(err.killed && input.timeoutMs), stdout: err.stdout ?? "", stderr: err.stderr ?? "", pid: null, startedAt, }; } }, }; await prepareCommandManagedRuntime({ runner, spec: { remoteCwd: remoteBaseDir, timeoutMs: 30_000, }, adapterKey: "codex", workspaceLocalDir: localWorkspaceDir, workspaceRemoteDir: remoteWorkspaceDir, }); expect(calls.length).toBeGreaterThan(0); expect(calls.every((call) => call.cwd === remoteBaseDir)).toBe(true); await expect(readFile(path.join(remoteWorkspaceDir, "README.md"), "utf8")).resolves.toBe("local workspace\n"); }); });