diff --git a/lib/sandbox/__tests__/processCreateSandbox.test.ts b/lib/sandbox/__tests__/processCreateSandbox.test.ts index 296e29c..8511ec9 100644 --- a/lib/sandbox/__tests__/processCreateSandbox.test.ts +++ b/lib/sandbox/__tests__/processCreateSandbox.test.ts @@ -181,6 +181,48 @@ describe("processCreateSandbox", () => { }); }); + it("converts prompt to opencode run command", async () => { + 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(triggerRunSandboxCommand).mockResolvedValue({ + id: "run_prompt123", + }); + + const result = await processCreateSandbox({ + accountId: "acc_123", + prompt: "create a hello world index.html", + }); + + expect(result).toEqual({ + sandboxId: "sbx_123", + sandboxStatus: "running", + timeout: 600000, + createdAt: "2024-01-01T00:00:00.000Z", + runId: "run_prompt123", + }); + expect(triggerRunSandboxCommand).toHaveBeenCalledWith({ + command: "opencode", + args: ["run", "create a hello world index.html"], + cwd: undefined, + sandboxId: "sbx_123", + accountId: "acc_123", + }); + }); + it("throws when createSandbox fails", async () => { vi.mocked(selectAccountSnapshots).mockResolvedValue([]); vi.mocked(createSandbox).mockRejectedValue(new Error("Sandbox creation failed")); diff --git a/lib/sandbox/__tests__/validateSandboxBody.test.ts b/lib/sandbox/__tests__/validateSandboxBody.test.ts index 7beb2e2..a504683 100644 --- a/lib/sandbox/__tests__/validateSandboxBody.test.ts +++ b/lib/sandbox/__tests__/validateSandboxBody.test.ts @@ -103,6 +103,60 @@ describe("validateSandboxBody", () => { }); }); + it("returns validated body when prompt is provided", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: "org_456", + authToken: "token", + }); + vi.mocked(safeParseJson).mockResolvedValue({ + prompt: "create a hello world index.html", + }); + + const request = createMockRequest(); + const result = await validateSandboxBody(request); + + expect(result).toEqual({ + accountId: "acc_123", + orgId: "org_456", + authToken: "token", + prompt: "create a hello world index.html", + }); + }); + + it("returns error response when both command and prompt are provided", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + }); + vi.mocked(safeParseJson).mockResolvedValue({ + command: "ls", + prompt: "do something", + }); + + const request = createMockRequest(); + const result = await validateSandboxBody(request); + + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(400); + }); + + it("returns error response when prompt is empty string", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "acc_123", + orgId: null, + authToken: "token", + }); + vi.mocked(safeParseJson).mockResolvedValue({ prompt: "" }); + + const request = createMockRequest(); + const result = await validateSandboxBody(request); + + expect(result).toBeInstanceOf(NextResponse); + expect((result as NextResponse).status).toBe(400); + }); + it("returns error response when command is empty string", async () => { vi.mocked(validateAuthContext).mockResolvedValue({ accountId: "acc_123", diff --git a/lib/sandbox/processCreateSandbox.ts b/lib/sandbox/processCreateSandbox.ts index ee1ba49..b9da0e8 100644 --- a/lib/sandbox/processCreateSandbox.ts +++ b/lib/sandbox/processCreateSandbox.ts @@ -4,7 +4,10 @@ import { selectAccountSnapshots } from "@/lib/supabase/account_snapshots/selectA import { triggerRunSandboxCommand } from "@/lib/trigger/triggerRunSandboxCommand"; import type { SandboxBody } from "@/lib/sandbox/validateSandboxBody"; -type ProcessCreateSandboxInput = Pick; +type ProcessCreateSandboxInput = Pick< + SandboxBody, + "accountId" | "command" | "args" | "cwd" | "prompt" +>; type ProcessCreateSandboxResult = SandboxCreatedResponse & { runId?: string }; /** @@ -17,7 +20,11 @@ type ProcessCreateSandboxResult = SandboxCreatedResponse & { runId?: string }; export async function processCreateSandbox( input: ProcessCreateSandboxInput, ): Promise { - const { accountId, command, args, cwd } = input; + const { accountId, prompt, cwd } = input; + + // Convert prompt shortcut to opencode command + const command = prompt ? "opencode" : input.command; + const args = prompt ? ["run", prompt] : input.args; // Get account's most recent snapshot if available const accountSnapshots = await selectAccountSnapshots(accountId); diff --git a/lib/sandbox/validateSandboxBody.ts b/lib/sandbox/validateSandboxBody.ts index 1d8c608..8e4fe97 100644 --- a/lib/sandbox/validateSandboxBody.ts +++ b/lib/sandbox/validateSandboxBody.ts @@ -5,11 +5,17 @@ import { validateAuthContext, type AuthContext } from "@/lib/auth/validateAuthCo import { safeParseJson } from "@/lib/networking/safeParseJson"; import { z } from "zod"; -export const sandboxBodySchema = z.object({ - command: z.string().min(1, "command cannot be empty").optional(), - args: z.array(z.string()).optional(), - cwd: z.string().optional(), -}); +export const sandboxBodySchema = z + .object({ + command: z.string().min(1, "command cannot be empty").optional(), + args: z.array(z.string()).optional(), + cwd: z.string().optional(), + prompt: z.string().min(1, "prompt cannot be empty").optional(), + }) + .refine(data => !(data.command && data.prompt), { + message: "Cannot specify both command and prompt", + path: ["prompt"], + }); export type SandboxBody = z.infer & AuthContext;