Refactor Foundry GitHub and sandbox flows

This commit is contained in:
Nathan Flurry 2026-03-12 10:51:33 -07:00
parent 4bccd5fc8d
commit ec8e816d0d
112 changed files with 4026 additions and 2715 deletions

View file

@ -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();
}
});
});

View file

@ -54,6 +54,7 @@ export function createTestStackDriver(overrides?: Partial<StackDriver>): StackDr
export function createTestGithubDriver(overrides?: Partial<GithubDriver>): GithubDriver {
return {
listPullRequests: async () => [],
getPrInfo: async () => null,
createPr: async (_repoPath, _headBranch, _title) => ({
number: 1,
url: `https://github.com/test/repo/pull/1`,
@ -101,6 +102,15 @@ export function createTestSandboxAgentClient(overrides?: Partial<SandboxAgentCli
nextCursor: undefined,
}),
createProcess: async () => defaultProcess,
runProcess: async () => ({
durationMs: 1,
exitCode: 0,
stderr: "",
stderrTruncated: false,
stdout: "",
stdoutTruncated: false,
timedOut: false,
}),
listProcesses: async () => ({ processes: [defaultProcess] }),
getProcessLogs: async () => defaultLogs,
stopProcess: async () => ({ ...defaultProcess, status: "exited", exitCode: 0, exitedAtMs: Date.now() }),
@ -127,11 +137,14 @@ export function createTestDaytonaClient(overrides?: Partial<DaytonaClientLike>):
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: "",
}),
...overrides,
};
}

View file

@ -1,31 +1,34 @@
import { describe, expect, it } from "vitest";
import {
githubStateKey,
historyKey,
organizationKey,
repositoryKey,
sandboxInstanceKey,
taskKey,
taskStatusSyncKey,
historyKey,
projectBranchSyncKey,
projectKey,
projectPrSyncKey,
sandboxInstanceKey,
workspaceKey,
userGithubDataKey,
} from "../src/actors/keys.js";
describe("actor keys", () => {
it("prefixes every key with workspace namespace", () => {
it("prefixes every key with organization namespace", () => {
const keys = [
workspaceKey("default"),
projectKey("default", "repo"),
organizationKey("default"),
repositoryKey("default", "repo"),
githubStateKey("default"),
taskKey("default", "repo", "task"),
sandboxInstanceKey("default", "daytona", "sbx"),
historyKey("default", "repo"),
projectPrSyncKey("default", "repo"),
projectBranchSyncKey("default", "repo"),
taskStatusSyncKey("default", "repo", "task", "sandbox-1", "session-1"),
];
for (const key of keys) {
expect(key[0]).toBe("ws");
expect(key[0]).toBe("org");
expect(key[1]).toBe("default");
}
});
it("uses a separate namespace for user-scoped GitHub auth", () => {
expect(userGithubDataKey("user-123")).toEqual(["user", "user-123", "github"]);
});
});

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizeParentBranch, parentLookupFromStack, sortBranchesForOverview } from "../src/actors/project/stack-model.js";
import { normalizeParentBranch, parentLookupFromStack, sortBranchesForOverview } from "../src/actors/repository/stack-model.js";
describe("stack-model", () => {
it("normalizes self-parent references to null", () => {

View file

@ -6,7 +6,7 @@ import { execFileSync } from "node:child_process";
import { setTimeout as delay } from "node:timers/promises";
import { describe, expect, it } from "vitest";
import { setupTest } from "rivetkit/test";
import { workspaceKey } from "../src/actors/keys.js";
import { organizationKey } from "../src/actors/keys.js";
import { registry } from "../src/actors/index.js";
import { createTestDriver } from "./helpers/test-driver.js";
import { createTestRuntimeContext } from "./helpers/test-context.js";
@ -41,10 +41,10 @@ describe("workspace isolation", () => {
createTestRuntimeContext(testDriver);
const { client } = await setupTest(t, registry);
const wsA = await client.workspace.getOrCreate(workspaceKey("alpha"), {
const wsA = await client.organization.getOrCreate(organizationKey("alpha"), {
createWithInput: "alpha",
});
const wsB = await client.workspace.getOrCreate(workspaceKey("beta"), {
const wsB = await client.organization.getOrCreate(organizationKey("beta"), {
createWithInput: "beta",
});

View file

@ -1,7 +1,7 @@
// @ts-nocheck
import { describe, expect, it } from "vitest";
import { setupTest } from "rivetkit/test";
import { workspaceKey } from "../src/actors/keys.js";
import { organizationKey } from "../src/actors/keys.js";
import { registry } from "../src/actors/index.js";
import { createTestDriver } from "./helpers/test-driver.js";
import { createTestRuntimeContext } from "./helpers/test-context.js";
@ -26,7 +26,7 @@ describe("workspace star sandbox agent repo", () => {
createTestRuntimeContext(testDriver);
const { client } = await setupTest(t, registry);
const ws = await client.workspace.getOrCreate(workspaceKey("alpha"), {
const ws = await client.organization.getOrCreate(organizationKey("alpha"), {
createWithInput: "alpha",
});