the universal sandbox runtime for agents and humans.
pocketenv.io
sandbox
openclaw
agent
claude-code
vercel-sandbox
deno-sandbox
cloudflare-sandbox
atproto
sprites
daytona
1import { vi, describe, it, expect, beforeEach } from "vitest";
2import consola from "consola";
3import { Sandbox } from "@pocketenv/sdk";
4import createSandbox from "./create";
5
6vi.mock("../lib/sdk", () => ({ configureSdk: vi.fn() }));
7vi.mock("../lib/expandRepo", () => ({
8 expandRepo: vi.fn((repo: string) => `https://github.com/${repo}`),
9}));
10vi.mock("../lib/sodium", () => ({
11 default: vi.fn().mockResolvedValue("encrypted-token"),
12}));
13vi.mock("../lib/redact", () => ({
14 default: vi.fn((val: string) => val),
15}));
16vi.mock("./ssh", () => ({ default: vi.fn().mockResolvedValue(undefined) }));
17vi.mock("../theme", () => ({
18 c: {
19 primary: (s: string | number) => String(s),
20 },
21}));
22vi.mock("consola", () => ({
23 default: { log: vi.fn(), success: vi.fn(), error: vi.fn() },
24}));
25vi.mock("@pocketenv/sdk", () => ({
26 Sandbox: { get: vi.fn(), configure: vi.fn(), create: vi.fn() },
27}));
28
29describe("createSandbox", () => {
30 const mockExit = vi
31 .spyOn(process, "exit")
32 .mockImplementation(() => undefined as never);
33
34 beforeEach(() => {
35 vi.clearAllMocks();
36 delete process.env.SPRITE_TOKEN;
37 delete process.env.DAYTONA_API_KEY;
38 delete process.env.DAYTONA_ORGANIZATION_ID;
39 delete process.env.DENO_DEPLOY_TOKEN;
40 delete process.env.VERCEL_API_TOKEN;
41 delete process.env.VERCEL_PROJECT_ID;
42 delete process.env.VERCEL_TEAM_ID;
43 delete process.env.MODAL_TOKEN_ID;
44 delete process.env.MODAL_TOKEN_SECRET;
45 });
46
47 it("creates a sandbox with default provider and logs success", async () => {
48 vi.mocked(Sandbox.create).mockResolvedValue({
49 data: { name: "my-sandbox" },
50 } as any);
51
52 await createSandbox("my-sandbox", {});
53
54 expect(Sandbox.create).toHaveBeenCalledWith(
55 expect.objectContaining({ name: "my-sandbox" }),
56 );
57 expect(consola.success).toHaveBeenCalledWith(
58 expect.stringContaining("my-sandbox"),
59 );
60 });
61
62 it("exits with error for unsupported provider", async () => {
63 await createSandbox("my-sandbox", { provider: "unsupported" });
64
65 expect(consola.error).toHaveBeenCalledWith(
66 expect.stringContaining("Unsupported provider"),
67 );
68 expect(mockExit).toHaveBeenCalledWith(1);
69 });
70
71 it("exits with error when SPRITE_TOKEN is missing for sprites provider", async () => {
72 await createSandbox("my-sandbox", { provider: "sprites" });
73
74 expect(consola.error).toHaveBeenCalledWith(
75 expect.stringContaining("SPRITE_TOKEN"),
76 );
77 expect(mockExit).toHaveBeenCalledWith(1);
78 });
79
80 it("creates sandbox with sprites provider when token is set", async () => {
81 process.env.SPRITE_TOKEN = "test-sprite-token";
82 vi.mocked(Sandbox.create).mockResolvedValue({
83 data: { name: "my-sandbox" },
84 } as any);
85
86 await createSandbox("my-sandbox", { provider: "sprites" });
87
88 expect(Sandbox.create).toHaveBeenCalledWith(
89 expect.objectContaining({
90 provider: "sprites",
91 providerOptions: expect.objectContaining({
92 spriteToken: "encrypted-token",
93 }),
94 }),
95 );
96 expect(consola.success).toHaveBeenCalledOnce();
97 });
98
99 it("exits with error when DAYTONA vars are missing", async () => {
100 await createSandbox("my-sandbox", { provider: "daytona" });
101
102 expect(consola.error).toHaveBeenCalledWith(
103 expect.stringContaining("DAYTONA_API_KEY"),
104 );
105 expect(mockExit).toHaveBeenCalledWith(1);
106 });
107
108 it("exits with error when DENO_DEPLOY_TOKEN is missing", async () => {
109 await createSandbox("my-sandbox", { provider: "deno" });
110
111 expect(consola.error).toHaveBeenCalledWith(
112 expect.stringContaining("DENO_DEPLOY_TOKEN"),
113 );
114 expect(mockExit).toHaveBeenCalledWith(1);
115 });
116
117 it("exits with error when VERCEL vars are missing", async () => {
118 await createSandbox("my-sandbox", { provider: "vercel" });
119
120 expect(consola.error).toHaveBeenCalledWith(
121 expect.stringContaining("VERCEL_API_TOKEN"),
122 );
123 expect(mockExit).toHaveBeenCalledWith(1);
124 });
125
126 it("expands repo shorthand when provided", async () => {
127 const { expandRepo } = await import("../lib/expandRepo");
128 vi.mocked(Sandbox.create).mockResolvedValue({
129 data: { name: "my-sandbox" },
130 } as any);
131
132 await createSandbox("my-sandbox", { repo: "owner/repo" });
133
134 expect(expandRepo).toHaveBeenCalledWith("owner/repo");
135 expect(Sandbox.create).toHaveBeenCalledWith(
136 expect.objectContaining({
137 repo: "https://github.com/owner/repo",
138 }),
139 );
140 });
141
142 it("connects via SSH after creation when ssh option is set", async () => {
143 const connectToSandbox = (await import("./ssh")).default;
144 vi.mocked(Sandbox.create).mockResolvedValue({
145 data: { name: "my-sandbox" },
146 waitUntilRunning: vi.fn().mockResolvedValue(undefined),
147 } as any);
148
149 await createSandbox("my-sandbox", { ssh: true });
150
151 expect(connectToSandbox).toHaveBeenCalledWith("my-sandbox");
152 });
153
154 it("logs error when sandbox creation throws", async () => {
155 vi.mocked(Sandbox.create).mockRejectedValue(new Error("API error"));
156
157 await createSandbox("my-sandbox", {});
158
159 expect(consola.error).toHaveBeenCalledWith(
160 expect.stringContaining("Failed to create sandbox"),
161 );
162 });
163});