mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-18 18:03:48 +00:00
feat: add opencode compatibility layer (#68)
This commit is contained in:
parent
cc5a9e0d73
commit
ef3e811c94
32 changed files with 18163 additions and 310 deletions
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Utilities for spawning sandbox-agent for OpenCode compatibility testing.
|
||||
* Mirrors the patterns from sdks/typescript/src/spawn.ts
|
||||
*/
|
||||
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { createServer, type AddressInfo, type Server } from "node:net";
|
||||
import { existsSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export interface SandboxAgentHandle {
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
child: ChildProcess;
|
||||
dispose: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the sandbox-agent binary in common locations
|
||||
*/
|
||||
function findBinary(): string | null {
|
||||
// Check environment variable first
|
||||
if (process.env.SANDBOX_AGENT_BIN) {
|
||||
const path = process.env.SANDBOX_AGENT_BIN;
|
||||
if (existsSync(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Check cargo build outputs (relative to tests/opencode-compat/helpers)
|
||||
const cargoPaths = [
|
||||
resolve(__dirname, "../../../../../../target/debug/sandbox-agent"),
|
||||
resolve(__dirname, "../../../../../../target/release/sandbox-agent"),
|
||||
];
|
||||
|
||||
for (const p of cargoPaths) {
|
||||
if (existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a free port on the given host
|
||||
*/
|
||||
async function getFreePort(host: string): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.unref();
|
||||
server.on("error", reject);
|
||||
server.listen(0, host, () => {
|
||||
const address = server.address() as AddressInfo;
|
||||
server.close(() => resolve(address.port));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the server to become healthy
|
||||
*/
|
||||
async function waitForHealth(
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
timeoutMs: number,
|
||||
child: ChildProcess
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
let lastError: string | undefined;
|
||||
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error("sandbox-agent exited before becoming healthy");
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/v1/health`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
lastError = `status ${response.status}`;
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for sandbox-agent health (${lastError ?? "unknown"})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for child process to exit
|
||||
*/
|
||||
async function waitForExit(child: ChildProcess, timeoutMs: number): Promise<boolean> {
|
||||
if (child.exitCode !== null) {
|
||||
return true;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => resolve(false), timeoutMs);
|
||||
child.once("exit", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export interface SpawnOptions {
|
||||
host?: string;
|
||||
port?: number;
|
||||
token?: string;
|
||||
timeoutMs?: number;
|
||||
env?: Record<string, string>;
|
||||
/** Enable OpenCode compatibility mode */
|
||||
opencodeCompat?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a sandbox-agent instance for testing.
|
||||
* Each test should spawn its own instance on a unique port.
|
||||
*/
|
||||
export async function spawnSandboxAgent(options: SpawnOptions = {}): Promise<SandboxAgentHandle> {
|
||||
const binaryPath = findBinary();
|
||||
if (!binaryPath) {
|
||||
throw new Error(
|
||||
"sandbox-agent binary not found. Run 'cargo build -p sandbox-agent' first or set SANDBOX_AGENT_BIN."
|
||||
);
|
||||
}
|
||||
|
||||
const host = options.host ?? "127.0.0.1";
|
||||
const port = options.port ?? (await getFreePort(host));
|
||||
const token = options.token ?? randomBytes(24).toString("hex");
|
||||
const timeoutMs = options.timeoutMs ?? 30_000;
|
||||
|
||||
const args = ["server", "--host", host, "--port", String(port), "--token", token];
|
||||
|
||||
const compatEnv = {
|
||||
OPENCODE_COMPAT_FIXED_TIME_MS: "1700000000000",
|
||||
OPENCODE_COMPAT_DIRECTORY: "/workspace",
|
||||
OPENCODE_COMPAT_WORKTREE: "/workspace",
|
||||
OPENCODE_COMPAT_HOME: "/home/opencode",
|
||||
OPENCODE_COMPAT_STATE: "/state/opencode",
|
||||
OPENCODE_COMPAT_CONFIG: "/config/opencode",
|
||||
OPENCODE_COMPAT_BRANCH: "main",
|
||||
};
|
||||
|
||||
const child = spawn(binaryPath, args, {
|
||||
stdio: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
...compatEnv,
|
||||
...(options.env ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Collect stderr for debugging
|
||||
let stderr = "";
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
if (process.env.SANDBOX_AGENT_TEST_LOGS) {
|
||||
process.stderr.write(text);
|
||||
}
|
||||
});
|
||||
if (process.env.SANDBOX_AGENT_TEST_LOGS) {
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
process.stderr.write(chunk.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = `http://${host}:${port}`;
|
||||
|
||||
try {
|
||||
await waitForHealth(baseUrl, token, timeoutMs, child);
|
||||
} catch (err) {
|
||||
child.kill("SIGKILL");
|
||||
if (stderr) {
|
||||
throw new Error(`${err}. Stderr: ${stderr}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const dispose = async () => {
|
||||
if (child.exitCode !== null) {
|
||||
return;
|
||||
}
|
||||
child.kill("SIGTERM");
|
||||
const exited = await waitForExit(child, 5_000);
|
||||
if (!exited) {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
};
|
||||
|
||||
return { baseUrl, token, child, dispose };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the sandbox-agent binary if it doesn't exist
|
||||
*/
|
||||
export async function buildSandboxAgent(): Promise<void> {
|
||||
const binaryPath = findBinary();
|
||||
if (binaryPath) {
|
||||
console.log(`sandbox-agent binary found at: ${binaryPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Building sandbox-agent...");
|
||||
const projectRoot = resolve(__dirname, "../../../../../..");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn("cargo", ["build", "-p", "sandbox-agent"], {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
SANDBOX_AGENT_SKIP_INSPECTOR: "1",
|
||||
},
|
||||
});
|
||||
|
||||
proc.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`cargo build failed with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on("error", reject);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue