/** * Filesystem-based test isolated sandbox provider. * * Uses a temp directory on the local filesystem as the "sandbox". * Intended for testing the isolated provider abstraction without * requiring a real remote environment. */ import { execFile, spawn } from "node:child_process"; import { copyFile, cp, mkdir, mkdtemp, rm, stat } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { createInterface } from "node:readline"; import { createIsolatedSandboxProvider, type ExecResult, type IsolatedSandboxHandle, type IsolatedSandboxProvider, } from "../SandboxProvider.js"; /** * Create a filesystem-based test isolated sandbox provider. * * The "sandbox" is a temp directory. `exec` runs shell commands in it, * `copyIn`/`copyFileOut` copy files between host and the temp dir, * and `close` removes the temp dir. */ export const testIsolated = (): IsolatedSandboxProvider => createIsolatedSandboxProvider({ name: "test-isolated", create: async (): Promise => { const sandboxRoot = await mkdtemp(join(tmpdir(), "sandcastle-test-")); const worktreePath = join(sandboxRoot, "workspace"); await mkdir(worktreePath, { recursive: true }); return { worktreePath, exec: ( command: string, options?: { onLine?: (line: string) => void; cwd?: string; sudo?: boolean; }, ): Promise => { if (options?.onLine) { const onLine = options.onLine; return new Promise((resolve, reject) => { const proc = spawn("sh", ["-c", command], { cwd: options?.cwd ?? worktreePath, stdio: ["ignore", "pipe", "pipe"], }); const stdoutChunks: string[] = []; const stderrChunks: string[] = []; const rl = createInterface({ input: proc.stdout! }); rl.on("line", (line) => { stdoutChunks.push(line); onLine(line); }); proc.stderr!.on("data", (chunk: Buffer) => { stderrChunks.push(chunk.toString()); }); proc.on("error", (error) => { reject(new Error(`exec failed: ${error.message}`)); }); proc.on("close", (code) => { resolve({ stdout: stdoutChunks.join("\n"), stderr: stderrChunks.join(""), exitCode: code ?? 0, }); }); }); } return new Promise((resolve, reject) => { execFile( "sh", ["-c", command], { cwd: options?.cwd ?? worktreePath, maxBuffer: 10 * 1024 * 1024, }, (error, stdout, stderr) => { if (error && error.code === undefined) { reject(new Error(`exec failed: ${error.message}`)); } else { resolve({ stdout: stdout.toString(), stderr: stderr.toString(), exitCode: typeof error?.code === "number" ? error.code : 0, }); } }, ); }); }, copyIn: async ( hostPath: string, sandboxPath: string, ): Promise => { const info = await stat(hostPath); if (info.isDirectory()) { await cp(hostPath, sandboxPath, { recursive: true }); } else { await mkdir(dirname(sandboxPath), { recursive: true }); await copyFile(hostPath, sandboxPath); } }, copyFileOut: async ( sandboxPath: string, hostPath: string, ): Promise => { await mkdir(dirname(hostPath), { recursive: true }); await copyFile(sandboxPath, hostPath); }, close: async (): Promise => { await rm(sandboxRoot, { recursive: true, force: true }); }, }; }, });