Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fork-vercel-sandbox-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Create template-backed Vercel Sandbox sessions with the platform fork API, so eve no longer needs to pass template snapshot IDs into new sessions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type * as Vercel from "#compiled/@vercel/sandbox/index.js";

export type VercelCreateOptions = NonNullable<Parameters<typeof Vercel.Sandbox.create>[0]>;

export type VercelForkOptions = Parameters<typeof Vercel.Sandbox.fork>[0];

export type VercelGetOptions = Parameters<typeof Vercel.Sandbox.get>[0];

export type VercelModule = typeof Vercel;
Expand Down
82 changes: 64 additions & 18 deletions packages/eve/src/execution/sandbox/bindings/vercel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,27 @@ function createMockSandbox(input: {
}

function createTestVercelSandbox(input: Parameters<typeof createVercelSandbox>[0] = {}) {
const originalLoadSandboxModule = input.loadSandboxModule;
const loadSandboxModule =
originalLoadSandboxModule === undefined
? undefined
: async () => {
const sandboxModule = await originalLoadSandboxModule();
return {
...sandboxModule,
Sandbox: {
...sandboxModule.Sandbox,
fork: async (forkOptions: unknown) =>
await sandboxModule.Sandbox.create(forkOptions as never),
},
} as never;
};

return createVercelSandbox({
...input,
createSandbox: async ({ createOptions, sandboxModule }) =>
await sandboxModule.Sandbox.create(createOptions),
loadSandboxModule,
});
}

Expand Down Expand Up @@ -465,6 +482,30 @@ describe("createVercelSandbox", () => {
expect(sandboxModule.Sandbox.create).not.toHaveBeenCalled();
});

it("rejects an unprovisioned template instead of forking from its base image", async () => {
const templateSandbox = createMockSandbox({ name: "template-key" });
const sandboxModule = {
Sandbox: {
create: vi.fn(),
fork: vi.fn(),
get: vi.fn().mockResolvedValue(templateSandbox),
},
};
const backend = createVercelSandbox({
loadSandboxModule: async () => sandboxModule as never,
});

await expect(
backend.create({
runtimeContext: { appRoot: "/tmp/test-app-root" },
sessionKey: "session-key",
templateKey: "template-key",
}),
).rejects.toBeInstanceOf(SandboxTemplateNotProvisionedError);

expect(sandboxModule.Sandbox.fork).not.toHaveBeenCalled();
});

it("removes paths through the sandbox filesystem API", async () => {
const templateSandbox = createMockSandbox({ name: "template" });
const sessionSandbox = createMockSandbox({ name: "session" });
Expand Down Expand Up @@ -504,7 +545,7 @@ describe("createVercelSandbox", () => {
expect(sessionSandbox.runCommand).not.toHaveBeenCalled();
});

it("applies a 30-minute default timeout to Sandbox.create", async () => {
it("applies a 30-minute default timeout to template creation and session forks", async () => {
const templateSandbox = createMockSandbox({ name: "template" });
const sessionSandbox = createMockSandbox({ name: "session" });
const create = vi
Expand Down Expand Up @@ -540,21 +581,20 @@ describe("createVercelSandbox", () => {
expect(sessionArgs?.[0]).toMatchObject({ timeout: 30 * 60 * 1_000 });
});

it("applies framework defaults to Sandbox.create when no createOptions are supplied", async () => {
const templateSandbox = createMockSandbox({ name: "template" });
it("creates template-backed sessions through Sandbox.fork", async () => {
const templateSandbox = createMockSandbox({ name: "template-key" });
const sessionSandbox = createMockSandbox({ name: "session" });
const create = vi
.fn()
.mockResolvedValueOnce(templateSandbox)
.mockResolvedValueOnce(sessionSandbox);
const create = vi.fn().mockResolvedValueOnce(templateSandbox);
const fork = vi.fn().mockResolvedValueOnce(sessionSandbox);
const sandboxModule = {
Sandbox: {
create,
fork,
get: vi.fn().mockResolvedValue(null),
},
};

const backend = createTestVercelSandbox({
const backend = createVercelSandbox({
loadSandboxModule: async () => sandboxModule as never,
});

Expand All @@ -570,19 +610,25 @@ describe("createVercelSandbox", () => {
templateKey: "template-key",
});

expect(create).toHaveBeenCalledTimes(2);
const [templateArgs, sessionArgs] = create.mock.calls;
expect(create).toHaveBeenCalledTimes(1);
expect(fork).toHaveBeenCalledTimes(1);
const [templateArgs] = create.mock.calls;
const [sessionArgs] = fork.mock.calls;
expect(templateArgs?.[0]).toMatchObject({
image: "vercel/eve:latest",
name: "template-key",
persistent: false,
timeout: 30 * 60 * 1_000,
});
expect(sessionArgs?.[0]).toMatchObject({
name: "session-key",
persistent: true,
sourceSandbox: "template-key",
timeout: 30 * 60 * 1_000,
source: { snapshotId: "template-snapshot", type: "snapshot" },
});
expect(sessionArgs?.[0]).not.toHaveProperty("image");
expect(sessionArgs?.[0]).not.toHaveProperty("runtime");
expect(sessionArgs?.[0]).not.toHaveProperty("source");
});

it("creates a fresh session without reading or snapshotting a template when templateKey is null", async () => {
Expand Down Expand Up @@ -655,7 +701,7 @@ describe("createVercelSandbox", () => {
expect(sessionSandbox.snapshot).not.toHaveBeenCalled();
});

it("forwards factory createOptions to both template and session Sandbox.create", async () => {
it("forwards factory createOptions to template creation and session forks", async () => {
const templateSandbox = createMockSandbox({ name: "template" });
const sessionSandbox = createMockSandbox({ name: "session" });
const create = vi
Expand Down Expand Up @@ -707,7 +753,7 @@ describe("createVercelSandbox", () => {
persistent: true,
ports: [3000, 4000],
resources: { vcpus: 2 },
source: { snapshotId: "template-snapshot", type: "snapshot" },
sourceSandbox: "template",
timeout: 600_000,
});
expect(templateSandbox.update).toHaveBeenCalledWith({ networkPolicy: "deny-all" });
Expand Down Expand Up @@ -765,7 +811,7 @@ describe("createVercelSandbox", () => {
});
expect(templateSandbox.snapshot).toHaveBeenCalledTimes(1);
expect(sessionArgs?.[0]).toMatchObject({
source: { snapshotId: "template-snapshot", type: "snapshot" },
sourceSandbox: "template",
});
});

Expand Down Expand Up @@ -813,7 +859,7 @@ describe("createVercelSandbox", () => {
expect(existingTemplate.snapshot).toHaveBeenCalledTimes(1);
expect(create).toHaveBeenCalledTimes(1);
expect(create.mock.calls[0]?.[0]).toMatchObject({
source: { snapshotId: "template-key-snapshot", type: "snapshot" },
sourceSandbox: "template-key",
});
});

Expand Down Expand Up @@ -886,15 +932,15 @@ describe("createVercelSandbox", () => {
expect(create).toHaveBeenCalledTimes(3);
expect(create.mock.calls[0]?.[0]).toMatchObject({
name: "session-key",
source: { snapshotId: "expired-template-snapshot", type: "snapshot" },
sourceSandbox: "template-key",
});
expect(create.mock.calls[1]?.[0]).toMatchObject({
name: "template-key",
persistent: false,
});
expect(create.mock.calls[2]?.[0]).toMatchObject({
name: "session-key",
source: { snapshotId: "template-key-snapshot", type: "snapshot" },
sourceSandbox: "template-key",
});
});

Expand Down Expand Up @@ -1067,7 +1113,7 @@ describe("createVercelSandbox", () => {
expect.objectContaining({
name: "deleted-sandbox",
persistent: true,
source: { snapshotId: "template-snapshot", type: "snapshot" },
sourceSandbox: "template-key",
}),
);
expect(handle.session).toBeDefined();
Expand Down
Loading
Loading