mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 10:02:26 +00:00
Refactor Foundry GitHub and sandbox flows
This commit is contained in:
parent
4bccd5fc8d
commit
ec8e816d0d
112 changed files with 4026 additions and 2715 deletions
|
|
@ -1,11 +1,24 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { DaytonaClientLike, DaytonaDriver } from "../src/driver.js";
|
||||
import type { DaytonaCreateSandboxOptions } from "../src/integrations/daytona/client.js";
|
||||
import { DaytonaProvider } from "../src/providers/daytona/index.js";
|
||||
|
||||
interface RecordedFetchCall {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
bodyText?: string;
|
||||
}
|
||||
|
||||
class RecordingDaytonaClient implements DaytonaClientLike {
|
||||
createSandboxCalls: DaytonaCreateSandboxOptions[] = [];
|
||||
executedCommands: string[] = [];
|
||||
getPreviewEndpointCalls: Array<{ sandboxId: string; port: number }> = [];
|
||||
executeCommandCalls: Array<{
|
||||
sandboxId: string;
|
||||
command: string;
|
||||
env?: Record<string, string>;
|
||||
timeoutSeconds?: number;
|
||||
}> = [];
|
||||
|
||||
async createSandbox(options: DaytonaCreateSandboxOptions) {
|
||||
this.createSandboxCalls.push(options);
|
||||
|
|
@ -32,17 +45,21 @@ class RecordingDaytonaClient implements DaytonaClientLike {
|
|||
|
||||
async deleteSandbox(_sandboxId: string) {}
|
||||
|
||||
async executeCommand(_sandboxId: string, command: string) {
|
||||
this.executedCommands.push(command);
|
||||
return { exitCode: 0, result: "" };
|
||||
}
|
||||
|
||||
async getPreviewEndpoint(sandboxId: string, port: number) {
|
||||
this.getPreviewEndpointCalls.push({ sandboxId, port });
|
||||
return {
|
||||
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
|
||||
token: "preview-token",
|
||||
};
|
||||
}
|
||||
|
||||
async executeCommand(sandboxId: string, command: string, env?: Record<string, string>, timeoutSeconds?: number) {
|
||||
this.executeCommandCalls.push({ sandboxId, command, env, timeoutSeconds });
|
||||
return {
|
||||
exitCode: 0,
|
||||
result: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createProviderWithClient(client: DaytonaClientLike): DaytonaProvider {
|
||||
|
|
@ -59,79 +76,159 @@ function createProviderWithClient(client: DaytonaClientLike): DaytonaProvider {
|
|||
);
|
||||
}
|
||||
|
||||
function withFetchStub(implementation: (call: RecordedFetchCall) => Response | Promise<Response>): () => void {
|
||||
const previous = globalThis.fetch;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
const headerRecord: Record<string, string> = {};
|
||||
headers.forEach((value, key) => {
|
||||
headerRecord[key] = value;
|
||||
});
|
||||
const bodyText = typeof init?.body === "string" ? init.body : init?.body instanceof Uint8Array ? Buffer.from(init.body).toString("utf8") : undefined;
|
||||
return await implementation({
|
||||
url: typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url,
|
||||
method: init?.method ?? "GET",
|
||||
headers: headerRecord,
|
||||
bodyText,
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
return () => {
|
||||
globalThis.fetch = previous;
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
|
||||
delete process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
|
||||
});
|
||||
|
||||
describe("daytona provider snapshot image behavior", () => {
|
||||
it("creates sandboxes using a snapshot-capable image recipe", async () => {
|
||||
it("creates sandboxes using a snapshot-capable image recipe and clones via sandbox-agent process api", async () => {
|
||||
const client = new RecordingDaytonaClient();
|
||||
const provider = createProviderWithClient(client);
|
||||
const fetchCalls: RecordedFetchCall[] = [];
|
||||
const restoreFetch = withFetchStub(async (call) => {
|
||||
fetchCalls.push(call);
|
||||
|
||||
const handle = await provider.createSandbox({
|
||||
workspaceId: "default",
|
||||
repoId: "repo-1",
|
||||
repoRemote: "https://github.com/acme/repo.git",
|
||||
branchName: "feature/test",
|
||||
taskId: "task-1",
|
||||
if (call.url.endsWith("/v1/health")) {
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (call.url.endsWith("/v1/processes/run")) {
|
||||
return new Response(JSON.stringify({ exitCode: 0, stdout: "", stderr: "" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${call.method} ${call.url}`);
|
||||
});
|
||||
|
||||
expect(client.createSandboxCalls).toHaveLength(1);
|
||||
const createCall = client.createSandboxCalls[0];
|
||||
if (!createCall) {
|
||||
throw new Error("expected create sandbox call");
|
||||
try {
|
||||
const handle = await provider.createSandbox({
|
||||
workspaceId: "default",
|
||||
repoId: "repo-1",
|
||||
repoRemote: "https://github.com/acme/repo.git",
|
||||
branchName: "feature/test",
|
||||
taskId: "task-1",
|
||||
githubToken: "github-token",
|
||||
});
|
||||
|
||||
expect(client.createSandboxCalls).toHaveLength(1);
|
||||
const createCall = client.createSandboxCalls[0];
|
||||
if (!createCall) {
|
||||
throw new Error("expected create sandbox call");
|
||||
}
|
||||
|
||||
expect(typeof createCall.image).not.toBe("string");
|
||||
if (typeof createCall.image === "string") {
|
||||
throw new Error("expected daytona image recipe object");
|
||||
}
|
||||
|
||||
const dockerfile = createCall.image.dockerfile;
|
||||
expect(dockerfile).toContain("apt-get install -y curl ca-certificates git openssh-client");
|
||||
expect(dockerfile).toContain("deb.nodesource.com/setup_20.x");
|
||||
expect(dockerfile).toContain("apt-get install -y nodejs");
|
||||
expect(dockerfile).toContain("sandbox-agent/0.3.0/install.sh");
|
||||
expect(dockerfile).toContain("sandbox-agent install-agent codex; sandbox-agent install-agent claude");
|
||||
expect(dockerfile).not.toContain("|| true");
|
||||
expect(dockerfile).not.toContain("ENTRYPOINT [");
|
||||
|
||||
expect(client.getPreviewEndpointCalls).toEqual([{ sandboxId: "sandbox-1", port: 2468 }]);
|
||||
expect(client.executeCommandCalls).toHaveLength(1);
|
||||
expect(client.executeCommandCalls[0]?.sandboxId).toBe("sandbox-1");
|
||||
expect(client.executeCommandCalls[0]?.command).toContain("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 2468");
|
||||
expect(fetchCalls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
||||
"GET https://preview.example/sandbox/sandbox-1/port/2468/v1/health",
|
||||
"POST https://preview.example/sandbox/sandbox-1/port/2468/v1/processes/run",
|
||||
]);
|
||||
|
||||
const runCall = fetchCalls[1];
|
||||
if (!runCall?.bodyText) {
|
||||
throw new Error("expected process run request body");
|
||||
}
|
||||
const runBody = JSON.parse(runCall.bodyText) as {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
expect(runBody.command).toBe("bash");
|
||||
expect(runBody.args).toHaveLength(2);
|
||||
expect(runBody.args[0]).toBe("-lc");
|
||||
expect(runBody.env).toEqual({
|
||||
GITHUB_TOKEN: "github-token",
|
||||
});
|
||||
expect(runBody.args[1]).toContain("GIT_TERMINAL_PROMPT=0");
|
||||
expect(runBody.args[1]).toContain('AUTH_REMOTE="$REMOTE"');
|
||||
expect(runBody.args[1]).toContain('git clone "$AUTH_REMOTE"');
|
||||
expect(runBody.args[1]).toContain('AUTH_HEADER="$(printf');
|
||||
|
||||
expect(handle.metadata.snapshot).toBe("snapshot-foundry");
|
||||
expect(handle.metadata.image).toBe("ubuntu:24.04");
|
||||
expect(handle.metadata.cwd).toBe("/home/daytona/foundry/default/repo-1/task-1/repo");
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
|
||||
expect(typeof createCall.image).not.toBe("string");
|
||||
if (typeof createCall.image === "string") {
|
||||
throw new Error("expected daytona image recipe object");
|
||||
}
|
||||
|
||||
const dockerfile = createCall.image.dockerfile;
|
||||
expect(dockerfile).toContain("apt-get install -y curl ca-certificates git openssh-client nodejs npm");
|
||||
expect(dockerfile).toContain("sandbox-agent/0.3.0/install.sh");
|
||||
const installAgentLines = dockerfile.match(/sandbox-agent install-agent [a-z0-9-]+/gi) ?? [];
|
||||
expect(installAgentLines.length).toBeGreaterThanOrEqual(2);
|
||||
const commands = client.executedCommands.join("\n");
|
||||
expect(commands).toContain("GIT_TERMINAL_PROMPT=0");
|
||||
expect(commands).toContain("GIT_ASKPASS=/bin/echo");
|
||||
|
||||
expect(handle.metadata.snapshot).toBe("snapshot-foundry");
|
||||
expect(handle.metadata.image).toBe("ubuntu:24.04");
|
||||
expect(handle.metadata.cwd).toBe("/home/daytona/foundry/default/repo-1/task-1/repo");
|
||||
expect(client.executedCommands.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("starts sandbox-agent with ACP timeout env override", async () => {
|
||||
const previous = process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
|
||||
it("ensures sandbox-agent by checking health through the preview endpoint", async () => {
|
||||
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = "240000";
|
||||
|
||||
try {
|
||||
const client = new RecordingDaytonaClient();
|
||||
const provider = createProviderWithClient(client);
|
||||
const client = new RecordingDaytonaClient();
|
||||
const provider = createProviderWithClient(client);
|
||||
const fetchCalls: RecordedFetchCall[] = [];
|
||||
const restoreFetch = withFetchStub(async (call) => {
|
||||
fetchCalls.push(call);
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
|
||||
await provider.ensureSandboxAgent({
|
||||
try {
|
||||
const endpoint = await provider.ensureSandboxAgent({
|
||||
workspaceId: "default",
|
||||
sandboxId: "sandbox-1",
|
||||
});
|
||||
|
||||
const startCommand = client.executedCommands.find((command) =>
|
||||
command.includes("nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000 sandbox-agent server"),
|
||||
);
|
||||
|
||||
const joined = client.executedCommands.join("\n");
|
||||
expect(joined).toContain("sandbox-agent/0.3.0/install.sh");
|
||||
expect(joined).toContain("SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000");
|
||||
expect(joined).toContain("apt-get install -y nodejs npm");
|
||||
expect(joined).toContain("sandbox-agent server --no-token --host 0.0.0.0 --port 2468");
|
||||
expect(startCommand).toBeTruthy();
|
||||
expect(endpoint).toEqual({
|
||||
endpoint: "https://preview.example/sandbox/sandbox-1/port/2468",
|
||||
token: "preview-token",
|
||||
});
|
||||
expect(client.executeCommandCalls).toHaveLength(1);
|
||||
expect(client.executeCommandCalls[0]?.command).toContain("nohup sandbox-agent server --no-token --host 0.0.0.0 --port 2468");
|
||||
expect(client.getPreviewEndpointCalls).toEqual([{ sandboxId: "sandbox-1", port: 2468 }]);
|
||||
expect(fetchCalls.map((call) => `${call.method} ${call.url}`)).toEqual(["GET https://preview.example/sandbox/sandbox-1/port/2468/v1/health"]);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = previous;
|
||||
}
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails with explicit timeout when daytona createSandbox hangs", async () => {
|
||||
const previous = process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
|
||||
process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS = "120";
|
||||
|
||||
const hangingClient: DaytonaClientLike = {
|
||||
|
|
@ -140,13 +237,20 @@ describe("daytona provider snapshot image behavior", () => {
|
|||
startSandbox: async () => {},
|
||||
stopSandbox: async () => {},
|
||||
deleteSandbox: async () => {},
|
||||
executeCommand: async () => ({ exitCode: 0, result: "" }),
|
||||
getPreviewEndpoint: async (sandboxId, port) => ({
|
||||
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
|
||||
token: "preview-token",
|
||||
}),
|
||||
executeCommand: async () => ({
|
||||
exitCode: 0,
|
||||
result: "",
|
||||
}),
|
||||
};
|
||||
|
||||
const restoreFetch = withFetchStub(async () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
try {
|
||||
const provider = createProviderWithClient(hangingClient);
|
||||
await expect(
|
||||
|
|
@ -159,26 +263,64 @@ describe("daytona provider snapshot image behavior", () => {
|
|||
}),
|
||||
).rejects.toThrow("daytona create sandbox timed out after 120ms");
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS = previous;
|
||||
}
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
|
||||
it("executes backend-managed sandbox commands through provider API", async () => {
|
||||
it("executes backend-managed sandbox commands through sandbox-agent process api", async () => {
|
||||
const client = new RecordingDaytonaClient();
|
||||
const provider = createProviderWithClient(client);
|
||||
const fetchCalls: RecordedFetchCall[] = [];
|
||||
const restoreFetch = withFetchStub(async (call) => {
|
||||
fetchCalls.push(call);
|
||||
|
||||
const result = await provider.executeCommand({
|
||||
workspaceId: "default",
|
||||
sandboxId: "sandbox-1",
|
||||
command: "echo backend-push",
|
||||
label: "manual push",
|
||||
if (call.url.endsWith("/v1/health")) {
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
if (call.url.endsWith("/v1/processes/run")) {
|
||||
return new Response(JSON.stringify({ exitCode: 0, stdout: "backend-push\n", stderr: "" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`unexpected fetch: ${call.method} ${call.url}`);
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(client.executedCommands).toContain("echo backend-push");
|
||||
try {
|
||||
const result = await provider.executeCommand({
|
||||
workspaceId: "default",
|
||||
sandboxId: "sandbox-1",
|
||||
command: "echo backend-push",
|
||||
env: { GITHUB_TOKEN: "user-token" },
|
||||
label: "manual push",
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.result).toBe("backend-push\n");
|
||||
expect(fetchCalls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
||||
"GET https://preview.example/sandbox/sandbox-1/port/2468/v1/health",
|
||||
"POST https://preview.example/sandbox/sandbox-1/port/2468/v1/processes/run",
|
||||
]);
|
||||
|
||||
const runCall = fetchCalls[1];
|
||||
if (!runCall?.bodyText) {
|
||||
throw new Error("expected process run body");
|
||||
}
|
||||
const runBody = JSON.parse(runCall.bodyText) as {
|
||||
command: string;
|
||||
args: string[];
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
expect(runBody.command).toBe("bash");
|
||||
expect(runBody.args).toEqual(["-lc", "echo backend-push"]);
|
||||
expect(runBody.env).toEqual({ GITHUB_TOKEN: "user-token" });
|
||||
} finally {
|
||||
restoreFetch();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue