import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; const { prepareWorkspaceForSshExecution, restoreWorkspaceFromSshExecution, runSshCommand, syncDirectoryToSsh, } = vi.hoisted(() => ({ prepareWorkspaceForSshExecution: vi.fn(async () => ({ gitBacked: false })), restoreWorkspaceFromSshExecution: vi.fn(async () => undefined), runSshCommand: vi.fn(async () => ({ stdout: Buffer.from('{"token":"remote"}\n').toString("base64"), stderr: "", })), syncDirectoryToSsh: vi.fn(async (_input: { localDir: string }) => undefined), })); vi.mock("./ssh.js", () => ({ prepareWorkspaceForSshExecution, restoreWorkspaceFromSshExecution, runSshCommand, syncDirectoryToSsh, })); import { prepareRemoteManagedRuntime } from "./remote-managed-runtime.js"; describe("remote managed runtime", () => { const cleanupDirs: string[] = []; afterEach(async () => { vi.clearAllMocks(); while (cleanupDirs.length > 0) { const dir = cleanupDirs.pop(); if (!dir) continue; await rm(dir, { recursive: true, force: true }).catch(() => undefined); } }); it("restores runtime assets without restoring an in-place SSH workspace", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-remote-runtime-assets-only-")); cleanupDirs.push(rootDir); const workspaceDir = path.join(rootDir, "workspace"); const homeDir = path.join(rootDir, "home"); await mkdir(workspaceDir, { recursive: true }); await mkdir(homeDir, { recursive: true }); await writeFile(path.join(homeDir, "auth.json"), '{"token":"host"}\n', "utf8"); let restoredAuth = ""; const prepared = await prepareRemoteManagedRuntime({ spec: { host: "127.0.0.1", port: 2222, username: "fixture", remoteWorkspacePath: "/app", remoteCwd: "/app", privateKey: "PRIVATE KEY", knownHosts: "KNOWN HOSTS", strictHostKeyChecking: true, }, runId: "run-in-place", adapterKey: "codex", workspaceLocalDir: workspaceDir, workspaceRemoteDir: "/app", syncWorkspace: false, assets: [ { key: "home", localDir: homeDir, restore: async ({ assetDir, readFile }) => { restoredAuth = (await readFile(path.posix.join(assetDir, "auth.json"))).toString("utf8"); }, }, ], }); expect(prepareWorkspaceForSshExecution).not.toHaveBeenCalled(); expect(syncDirectoryToSsh).toHaveBeenCalledWith(expect.objectContaining({ localDir: homeDir, remoteDir: "/app/.paperclip-runtime/codex/home", })); await prepared.restoreWorkspace(); expect(restoreWorkspaceFromSshExecution).not.toHaveBeenCalled(); expect(runSshCommand).toHaveBeenCalledWith( expect.anything(), "base64 < '/app/.paperclip-runtime/codex/home/auth.json'", { maxBuffer: 1024 * 1024 }, ); expect(restoredAuth).toBe('{"token":"remote"}\n'); }); it("stages each additional project into its own isolated SSH dir, isolating one failure", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-remote-runtime-additional-")); cleanupDirs.push(rootDir); const workspaceDir = path.join(rootDir, "workspace"); const firstDir = path.join(rootDir, "referenced-first"); const secondDir = path.join(rootDir, "referenced-second"); const brokenDir = path.join(rootDir, "referenced-broken"); await mkdir(workspaceDir, { recursive: true }); // The transfer rejects only for the broken project's directory. syncDirectoryToSsh.mockImplementation(async (input: { localDir: string }) => { if (input.localDir === brokenDir) throw new Error("ssh transfer failed"); return undefined; }); const prepared = await prepareRemoteManagedRuntime({ spec: { host: "127.0.0.1", port: 2222, username: "fixture", remoteWorkspacePath: "/app", remoteCwd: "/app", privateKey: "PRIVATE KEY", knownHosts: "KNOWN HOSTS", strictHostKeyChecking: true, }, runId: "run-additional", adapterKey: "codex", workspaceLocalDir: workspaceDir, workspaceRemoteDir: "/app", syncWorkspace: false, additionalSources: [ { localPath: firstDir, projectId: "first" }, { localPath: brokenDir, projectId: "broken" }, { localPath: secondDir, projectId: "second" }, ], }); // Each healthy project staged into its OWN isolated dir under the runtime // root; the broken one is skipped, not fatal. expect(Object.keys(prepared.additionalSourceDirs).sort()).toEqual(["first", "second"]); expect(prepared.additionalSourceDirs.first).toBe("/app/.paperclip-runtime/codex/project-first"); expect(prepared.additionalSourceDirs.second).toBe("/app/.paperclip-runtime/codex/project-second"); expect(prepared.additionalSourceDirs.broken).toBeUndefined(); expect(syncDirectoryToSsh).toHaveBeenCalledWith(expect.objectContaining({ localDir: firstDir, remoteDir: "/app/.paperclip-runtime/codex/project-first", })); expect(syncDirectoryToSsh).toHaveBeenCalledWith(expect.objectContaining({ localDir: secondDir, remoteDir: "/app/.paperclip-runtime/codex/project-second", })); }); it("skips an additional project whose localPath is not absolute", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-remote-runtime-relative-")); cleanupDirs.push(rootDir); const workspaceDir = path.join(rootDir, "workspace"); const healthyDir = path.join(rootDir, "referenced-healthy"); await mkdir(workspaceDir, { recursive: true }); const prepared = await prepareRemoteManagedRuntime({ spec: { host: "127.0.0.1", port: 2222, username: "fixture", remoteWorkspacePath: "/app", remoteCwd: "/app", privateKey: "PRIVATE KEY", knownHosts: "KNOWN HOSTS", strictHostKeyChecking: true, }, runId: "run-relative", adapterKey: "codex", workspaceLocalDir: workspaceDir, workspaceRemoteDir: "/app", syncWorkspace: false, additionalSources: [ { localPath: "relative/referenced", projectId: "relative" }, { localPath: healthyDir, projectId: "healthy" }, ], }); // The relative-path project never reaches the transfer and is skipped; the // absolute-path project still stages. expect(Object.keys(prepared.additionalSourceDirs)).toEqual(["healthy"]); expect(syncDirectoryToSsh).not.toHaveBeenCalledWith(expect.objectContaining({ localDir: "relative/referenced", })); }); });