sandbox-agent/foundry/packages/backend/test/daytona-provider.test.ts
Nathan Flurry 098b8113f3 Improve Daytona sandbox provisioning and frontend UI
Refactor git clone script in Daytona provider to use cleaner shell logic for GitHub token authentication and branch checkout. Add support for private repository clones with token-based auth. Improve Daytona provider error handling and git configuration setup.

Frontend improvements include enhanced dev panel, workspace dashboard, sidebar navigation, and UI components for better task/session management. Update interest manager and backend client to support improved session state handling.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-13 23:06:24 -07:00

205 lines
7.4 KiB
TypeScript

import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { 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";
class RecordingDaytonaClient implements DaytonaClientLike {
createSandboxCalls: DaytonaCreateSandboxOptions[] = [];
executedCommands: string[] = [];
async createSandbox(options: DaytonaCreateSandboxOptions) {
this.createSandboxCalls.push(options);
return {
id: "sandbox-1",
state: "started",
snapshot: "snapshot-foundry",
labels: {},
};
}
async getSandbox(sandboxId: string) {
return {
id: sandboxId,
state: "started",
snapshot: "snapshot-foundry",
labels: {},
};
}
async startSandbox(_sandboxId: string, _timeoutSeconds?: number) {}
async stopSandbox(_sandboxId: string, _timeoutSeconds?: number) {}
async deleteSandbox(_sandboxId: string) {}
async executeCommand(_sandboxId: string, command: string) {
this.executedCommands.push(command);
return { exitCode: 0, result: "" };
}
async getPreviewEndpoint(sandboxId: string, port: number) {
return {
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
token: "preview-token",
};
}
}
function createProviderWithClient(client: DaytonaClientLike): DaytonaProvider {
const daytonaDriver: DaytonaDriver = {
createClient: () => client,
};
return new DaytonaProvider(
{
apiKey: "test-key",
image: "ubuntu:24.04",
},
daytonaDriver,
);
}
describe("daytona provider snapshot image behavior", () => {
it("creates sandboxes using a snapshot-capable image recipe", async () => {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
const handle = await provider.createSandbox({
workspaceId: "default",
repoId: "repo-1",
repoRemote: "https://github.com/acme/repo.git",
branchName: "feature/test",
taskId: "task-1",
});
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 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(commands).not.toContain("[[");
expect(commands).not.toContain("GIT_AUTH_ARGS=()");
expect(commands).not.toContain("${GIT_AUTH_ARGS[@]}");
expect(commands).not.toContain(".extraheader");
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;
const previousHome = process.env.HOME;
const tempHome = resolve(tmpdir(), `daytona-provider-test-${Date.now()}`);
mkdirSync(resolve(tempHome, ".codex"), { recursive: true });
writeFileSync(resolve(tempHome, ".codex", "auth.json"), JSON.stringify({ access_token: "test-token" }));
process.env.HOME = tempHome;
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = "240000";
try {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
await provider.ensureSandboxAgent({
workspaceId: "default",
sandboxId: "sandbox-1",
});
const startCommand = client.executedCommands.find(
(command) => command.includes("export SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=") && command.includes("sandbox-agent server --no-token"),
);
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");
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(joined).toContain('mkdir -p "$HOME/.codex" "$HOME/.config/codex"');
expect(joined).toContain("unset OPENAI_API_KEY CODEX_API_KEY");
expect(joined).not.toContain('rm -f "$HOME/.codex/auth.json"');
expect(startCommand).toBeTruthy();
} 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;
}
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
rmSync(tempHome, { force: true, recursive: true });
}
});
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 = {
createSandbox: async () => await new Promise(() => {}),
getSandbox: async (sandboxId) => ({ id: sandboxId, state: "started" }),
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",
}),
};
try {
const provider = createProviderWithClient(hangingClient);
await expect(
provider.createSandbox({
workspaceId: "default",
repoId: "repo-1",
repoRemote: "https://github.com/acme/repo.git",
branchName: "feature/test",
taskId: "task-timeout",
}),
).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;
}
}
});
it("executes backend-managed sandbox commands through provider API", async () => {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
const result = await provider.executeCommand({
workspaceId: "default",
sandboxId: "sandbox-1",
command: "echo backend-push",
label: "manual push",
});
expect(result.exitCode).toBe(0);
expect(client.executedCommands).toContain("echo backend-push");
});
});