mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
|
|
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<string, string>;
|
||
|
|
stdin?: string;
|
||
|
|
timeoutMs?: number;
|
||
|
|
}> = [];
|
||
|
|
const runner = {
|
||
|
|
execute: async (input: {
|
||
|
|
command: string;
|
||
|
|
args?: string[];
|
||
|
|
cwd?: string;
|
||
|
|
env?: Record<string, string>;
|
||
|
|
stdin?: string;
|
||
|
|
timeoutMs?: number;
|
||
|
|
}): Promise<RunProcessResult> => {
|
||
|
|
calls.push({ ...input });
|
||
|
|
const startedAt = new Date().toISOString();
|
||
|
|
const env = {
|
||
|
|
...process.env,
|
||
|
|
...input.env,
|
||
|
|
};
|
||
|
|
const command = input.command === "sh" ? "/bin/sh" : input.command;
|
||
|
|
const args = [...(input.args ?? [])];
|
||
|
|
if (input.stdin != null && input.command === "sh" && 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);
|
||
|
|
});
|
||
|
|
});
|