Skip to content
106 changes: 99 additions & 7 deletions lib/sandbox/__tests__/createSandboxPostHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody";
import { createSandbox } from "@/lib/sandbox/createSandbox";
import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox";
import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand";
import { triggerSetupSandbox } from "@/lib/trigger/triggerSetupSandbox";
import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots";

vi.mock("@/lib/sandbox/validateSandboxBody", () => ({
Expand All @@ -25,6 +26,10 @@ vi.mock("@/lib/trigger/triggerRunSandboxCommand", () => ({
triggerRunSandboxCommand: vi.fn(),
}));

vi.mock("@/lib/trigger/triggerSetupSandbox", () => ({
triggerSetupSandbox: vi.fn(),
}));

vi.mock("@/lib/supabase/account_snapshots/selectAccountSnapshots", () => ({
selectAccountSnapshots: vi.fn(),
}));
Expand Down Expand Up @@ -56,7 +61,7 @@ describe("createSandboxPostHandler", () => {
expect(response.status).toBe(401);
});

it("returns 200 with sandboxes array including runId on success", async () => {
it("returns runId from command task when command is provided", async () => {
vi.mocked(validateSandboxBody).mockResolvedValue({
accountId: "acc_123",
orgId: null,
Expand All @@ -79,6 +84,9 @@ describe("createSandboxPostHandler", () => {
},
error: null,
});
vi.mocked(triggerSetupSandbox).mockResolvedValue({
id: "setup_abc123",
});
vi.mocked(triggerRunSandboxCommand).mockResolvedValue({
id: "run_abc123",
});
Expand Down Expand Up @@ -302,12 +310,11 @@ describe("createSandboxPostHandler", () => {
});
});

it("returns 200 without runId and skips trigger when command is not provided", async () => {
it("returns runId from setup task when no command is provided", async () => {
vi.mocked(validateSandboxBody).mockResolvedValue({
accountId: "acc_123",
orgId: null,
authToken: "token",
// command is not provided (optional)
});
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);
vi.mocked(createSandbox).mockResolvedValue({
Expand All @@ -325,6 +332,9 @@ describe("createSandboxPostHandler", () => {
},
error: null,
});
vi.mocked(triggerSetupSandbox).mockResolvedValue({
id: "setup_abc123",
});

const request = createMockRequest();
const response = await createSandboxPostHandler(request);
Expand All @@ -339,12 +349,15 @@ describe("createSandboxPostHandler", () => {
sandboxStatus: "running",
timeout: 600000,
createdAt: "2024-01-01T00:00:00.000Z",
// Note: runId is not included when command is not provided
runId: "setup_abc123",
},
],
});
// Verify triggerRunSandboxCommand was NOT called
expect(triggerRunSandboxCommand).not.toHaveBeenCalled();
expect(triggerSetupSandbox).toHaveBeenCalledWith({
sandboxId: "sbx_123",
accountId: "acc_123",
});
});

it("returns 200 without runId when triggerRunSandboxCommand throws", async () => {
Expand All @@ -370,12 +383,92 @@ describe("createSandboxPostHandler", () => {
},
error: null,
});
vi.mocked(triggerSetupSandbox).mockResolvedValue({
id: "setup_abc123",
});
vi.mocked(triggerRunSandboxCommand).mockRejectedValue(new Error("Task trigger failed"));

const request = createMockRequest();
const response = await createSandboxPostHandler(request);

// Sandbox was created successfully, so return 200 even if trigger fails
// Sandbox was created successfully, so return 200 even if command trigger fails
expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual({
status: "success",
sandboxes: [
{
sandboxId: "sbx_123",
sandboxStatus: "running",
timeout: 600000,
createdAt: "2024-01-01T00:00:00.000Z",
},
],
});
});

it("calls triggerSetupSandbox with sandboxId and accountId on every creation", async () => {
vi.mocked(validateSandboxBody).mockResolvedValue({
accountId: "acc_123",
orgId: null,
authToken: "token",
});
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);
vi.mocked(createSandbox).mockResolvedValue({
sandboxId: "sbx_789",
sandboxStatus: "running",
timeout: 600000,
createdAt: "2024-01-01T00:00:00.000Z",
});
vi.mocked(insertAccountSandbox).mockResolvedValue({
data: {
id: "record_123",
account_id: "acc_123",
sandbox_id: "sbx_789",
created_at: "2024-01-01T00:00:00.000Z",
},
error: null,
});
vi.mocked(triggerSetupSandbox).mockResolvedValue({
id: "setup_xyz",
});

const request = createMockRequest();
await createSandboxPostHandler(request);

expect(triggerSetupSandbox).toHaveBeenCalledWith({
sandboxId: "sbx_789",
accountId: "acc_123",
});
});

it("returns 200 without runId when triggerSetupSandbox throws and no command", async () => {
vi.mocked(validateSandboxBody).mockResolvedValue({
accountId: "acc_123",
orgId: null,
authToken: "token",
});
vi.mocked(selectAccountSnapshots).mockResolvedValue([]);
vi.mocked(createSandbox).mockResolvedValue({
sandboxId: "sbx_123",
sandboxStatus: "running",
timeout: 600000,
createdAt: "2024-01-01T00:00:00.000Z",
});
vi.mocked(insertAccountSandbox).mockResolvedValue({
data: {
id: "record_123",
account_id: "acc_123",
sandbox_id: "sbx_123",
created_at: "2024-01-01T00:00:00.000Z",
},
error: null,
});
vi.mocked(triggerSetupSandbox).mockRejectedValue(new Error("Setup trigger failed"));

const request = createMockRequest();
const response = await createSandboxPostHandler(request);

expect(response.status).toBe(200);
const json = await response.json();
expect(json).toEqual({
Expand All @@ -386,7 +479,6 @@ describe("createSandboxPostHandler", () => {
sandboxStatus: "running",
timeout: 600000,
createdAt: "2024-01-01T00:00:00.000Z",
// Note: runId is not included when trigger fails
},
],
});
Expand Down
15 changes: 14 additions & 1 deletion lib/sandbox/createSandboxPostHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createSandbox } from "@/lib/sandbox/createSandbox";
import { validateSandboxBody } from "@/lib/sandbox/validateSandboxBody";
import { insertAccountSandbox } from "@/lib/supabase/account_sandboxes/insertAccountSandbox";
import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand";
import { triggerSetupSandbox } from "@/lib/trigger/triggerSetupSandbox";
import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectAccountSnapshots";

/**
Expand Down Expand Up @@ -40,8 +41,19 @@ export async function createSandboxPostHandler(request: NextRequest): Promise<Ne
sandbox_id: result.sandboxId,
});

// Trigger the command execution task only if a command was provided
// Trigger the setup-sandbox task (fire-and-forget)
let runId: string | undefined;
try {
const setupHandle = await triggerSetupSandbox({
sandboxId: result.sandboxId,
accountId: validated.accountId,
});
runId = setupHandle.id;
} catch (triggerError) {
console.error("Failed to trigger setup-sandbox task:", triggerError);
}

// Trigger the command execution task if a command was provided (overrides runId)
if (validated.command) {
try {
const handle = await triggerRunSandboxCommand({
Expand All @@ -54,6 +66,7 @@ export async function createSandboxPostHandler(request: NextRequest): Promise<Ne
runId = handle.id;
} catch (triggerError) {
console.error("Failed to trigger run-sandbox-command task:", triggerError);
runId = undefined;
}
Comment on lines 67 to 70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resetting runId to undefined on command failure discards a valid setup runId.

If triggerSetupSandbox succeeded and provided a runId, but triggerRunSandboxCommand then fails, the client loses all tracking information. The setup task is still running — returning its runId would let the client at least monitor that. Consider omitting the runId = undefined reset so the setup runId falls through as a useful fallback.

🐛 Proposed fix
      } catch (triggerError) {
        console.error("Failed to trigger run-sandbox-command task:", triggerError);
-        runId = undefined;
+        // Keep the setup runId so the client can still track sandbox initialization
      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (triggerError) {
console.error("Failed to trigger run-sandbox-command task:", triggerError);
runId = undefined;
}
} catch (triggerError) {
console.error("Failed to trigger run-sandbox-command task:", triggerError);
// Keep the setup runId so the client can still track sandbox initialization
}
🤖 Prompt for AI Agents
In `@lib/sandbox/createSandboxPostHandler.ts` around lines 67 - 70, The catch
block that handles errors from triggerRunSandboxCommand currently resets runId
to undefined, which discards any valid runId returned earlier by
triggerSetupSandbox; remove the assignment runId = undefined from the catch so
that if triggerRunSandboxCommand fails the previously obtained runId is
preserved and can be returned to the client for monitoring; ensure the catch
still logs the triggerError (console.error("Failed to trigger
run-sandbox-command task:", triggerError)) but does not modify the runId
variable.

}

Expand Down
42 changes: 42 additions & 0 deletions lib/trigger/__tests__/triggerSetupSandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { tasks } from "@trigger.dev/sdk";
import { triggerSetupSandbox } from "../triggerSetupSandbox";

vi.mock("@trigger.dev/sdk", () => ({
tasks: {
trigger: vi.fn(),
},
}));

describe("triggerSetupSandbox", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("triggers setup-sandbox task with correct payload", async () => {
const mockHandle = { id: "run_123" };
vi.mocked(tasks.trigger).mockResolvedValue(mockHandle);

const payload = {
sandboxId: "sbx_456",
accountId: "acc_123",
};

const result = await triggerSetupSandbox(payload);

expect(tasks.trigger).toHaveBeenCalledWith("setup-sandbox", payload);
expect(result).toEqual(mockHandle);
});

it("passes through the task handle from trigger", async () => {
const mockHandle = { id: "run_789", publicAccessToken: "token_abc" };
vi.mocked(tasks.trigger).mockResolvedValue(mockHandle);

const result = await triggerSetupSandbox({
sandboxId: "sbx_999",
accountId: "acc_456",
});

expect(result).toBe(mockHandle);
});
});
17 changes: 17 additions & 0 deletions lib/trigger/triggerSetupSandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { tasks } from "@trigger.dev/sdk";

type SetupSandboxPayload = {
sandboxId: string;
accountId: string;
};

/**
* Triggers the setup-sandbox task to provision a newly created sandbox.
*
* @param payload - The task payload with sandboxId and accountId
* @returns The task handle with runId
*/
export async function triggerSetupSandbox(payload: SetupSandboxPayload) {
const handle = await tasks.trigger("setup-sandbox", payload);
return handle;
}