Skip to content

Commit 0ef64ed

Browse files
sweetmantechclaude
andcommitted
refactor: extract createSandboxWithFallback to own lib file, add error logging
- Extract createSandboxWithFallback to lib/sandbox/createSandboxWithFallback.ts (SRP) - processCreateSandbox and createSandboxFromSnapshot use it / follow same pattern - Log snapshot creation errors before falling back to fresh sandbox - Add 4 unit tests for createSandboxWithFallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c48d9f8 commit 0ef64ed

File tree

4 files changed

+103
-23
lines changed

4 files changed

+103
-23
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
3+
import { createSandboxWithFallback } from "../createSandboxWithFallback";
4+
5+
const mockCreateSandbox = vi.fn();
6+
7+
vi.mock("@/lib/sandbox/createSandbox", () => ({
8+
createSandbox: (...args: unknown[]) => mockCreateSandbox(...args),
9+
}));
10+
11+
const mockResponse = {
12+
sandboxId: "sbx_123",
13+
sandboxStatus: "running",
14+
timeout: 1800000,
15+
createdAt: "2024-01-01T00:00:00.000Z",
16+
};
17+
18+
describe("createSandboxWithFallback", () => {
19+
beforeEach(() => {
20+
vi.clearAllMocks();
21+
mockCreateSandbox.mockResolvedValue({
22+
sandbox: {},
23+
response: mockResponse,
24+
});
25+
});
26+
27+
it("creates from snapshot when snapshotId is provided", async () => {
28+
const result = await createSandboxWithFallback("snap_abc");
29+
30+
expect(mockCreateSandbox).toHaveBeenCalledWith({
31+
source: { type: "snapshot", snapshotId: "snap_abc" },
32+
});
33+
expect(result).toEqual(mockResponse);
34+
});
35+
36+
it("creates fresh sandbox when snapshotId is undefined", async () => {
37+
const result = await createSandboxWithFallback(undefined);
38+
39+
expect(mockCreateSandbox).toHaveBeenCalledWith({});
40+
expect(result).toEqual(mockResponse);
41+
});
42+
43+
it("falls back to fresh sandbox when snapshot creation fails", async () => {
44+
const freshResponse = { ...mockResponse, sandboxId: "sbx_fresh" };
45+
mockCreateSandbox
46+
.mockRejectedValueOnce(new Error("Status code 400 is not ok"))
47+
.mockResolvedValueOnce({ sandbox: {}, response: freshResponse });
48+
49+
const result = await createSandboxWithFallback("snap_bad");
50+
51+
expect(mockCreateSandbox).toHaveBeenCalledTimes(2);
52+
expect(result).toEqual(freshResponse);
53+
});
54+
55+
it("logs error when snapshot creation fails", async () => {
56+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
57+
const snapshotError = new Error("Status code 400 is not ok");
58+
mockCreateSandbox
59+
.mockRejectedValueOnce(snapshotError)
60+
.mockResolvedValueOnce({ sandbox: {}, response: mockResponse });
61+
62+
await createSandboxWithFallback("snap_bad");
63+
64+
expect(consoleSpy).toHaveBeenCalledWith(
65+
"Snapshot sandbox creation failed, falling back to fresh sandbox:",
66+
snapshotError,
67+
);
68+
consoleSpy.mockRestore();
69+
});
70+
});

lib/sandbox/createSandboxFromSnapshot.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ export async function createSandboxFromSnapshot(
2727
try {
2828
sandbox = (await createSandbox({ source: { type: "snapshot", snapshotId } })).sandbox;
2929
fromSnapshot = true;
30-
} catch {
31-
// Snapshot invalid or expired on Vercel's side — fall through to fresh
30+
} catch (error) {
31+
console.error(
32+
"Snapshot sandbox creation failed, falling back to fresh sandbox:",
33+
error,
34+
);
3235
}
3336
}
3437

@@ -38,8 +41,8 @@ export async function createSandboxFromSnapshot(
3841

3942
await insertAccountSandbox({
4043
account_id: accountId,
41-
sandbox_id: sandbox.sandboxId,
44+
sandbox_id: sandbox!.sandboxId,
4245
});
4346

44-
return { sandbox, fromSnapshot };
47+
return { sandbox: sandbox!, fromSnapshot };
4548
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createSandbox, type SandboxCreatedResponse } from "@/lib/sandbox/createSandbox";
2+
3+
/**
4+
* Attempts to create a sandbox from the given snapshot, falling back to a fresh sandbox on failure.
5+
* If no snapshotId is provided, creates a fresh sandbox directly.
6+
*
7+
* @param snapshotId - Optional snapshot ID to restore from
8+
* @returns The sandbox creation response
9+
*/
10+
export async function createSandboxWithFallback(
11+
snapshotId: string | undefined,
12+
): Promise<SandboxCreatedResponse> {
13+
if (snapshotId) {
14+
try {
15+
return (await createSandbox({ source: { type: "snapshot", snapshotId } })).response;
16+
} catch (error) {
17+
console.error(
18+
"Snapshot sandbox creation failed, falling back to fresh sandbox:",
19+
error,
20+
);
21+
}
22+
}
23+
return (await createSandbox({})).response;
24+
}

lib/sandbox/processCreateSandbox.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
1-
import { createSandbox, type SandboxCreatedResponse } from "@/lib/sandbox/createSandbox";
1+
import { createSandboxWithFallback } from "@/lib/sandbox/createSandboxWithFallback";
22
import { getValidSnapshotId } from "@/lib/sandbox/getValidSnapshotId";
33
import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox";
44
import { triggerPromptSandbox } from "@/lib/trigger/triggerPromptSandbox";
5+
import type { SandboxCreatedResponse } from "@/lib/sandbox/createSandbox";
56

67
type ProcessCreateSandboxInput = {
78
accountId: string;
89
prompt?: string;
910
};
1011
type ProcessCreateSandboxResult = SandboxCreatedResponse & { runId?: string };
1112

12-
/**
13-
* Attempts to create a sandbox from the given snapshot, falling back to a fresh sandbox on failure.
14-
* If no snapshotId is provided, creates a fresh sandbox directly.
15-
*
16-
* @param snapshotId - Optional snapshot ID to restore from
17-
* @returns The sandbox creation response
18-
*/
19-
async function createSandboxWithFallback(snapshotId: string | undefined) {
20-
if (snapshotId) {
21-
try {
22-
return (await createSandbox({ source: { type: "snapshot", snapshotId } })).response;
23-
} catch {
24-
// Snapshot invalid or expired on Vercel's side — fall through to fresh
25-
}
26-
}
27-
return (await createSandbox({})).response;
28-
}
29-
3013
/**
3114
* Shared domain logic for creating a sandbox and optionally running a prompt.
3215
* Used by both POST /api/sandboxes handler and the prompt_sandbox MCP tool.

0 commit comments

Comments
 (0)