Skip to content
Closed
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
50 changes: 50 additions & 0 deletions lib/sandbox/__tests__/createSandboxHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,56 @@ describe("createSandboxHandler", () => {
});
});

it("forwards `branch` (existing branch) to the source as branch when isNewBranch is false", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: {
repoUrl: "https://github.com/o/r",
sessionId: "sess-1",
branch: "develop",
isNewBranch: false,
},
auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" },
});

await createSandboxHandler(makeReq());

const args = vi.mocked(connectSandbox).mock.calls[0]?.[0] as {
state: { source?: { repo: string; branch?: string; newBranch?: string } };
};
expect(args.state.source).toMatchObject({ repo: "https://github.com/o/r", branch: "develop" });
expect(args.state.source?.newBranch).toBeUndefined();
});

it("forwards `branch` to the source as newBranch when isNewBranch is true", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: {
repoUrl: "https://github.com/o/r",
sessionId: "sess-1",
branch: "xy/abcd1234",
isNewBranch: true,
},
auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" },
});

await createSandboxHandler(makeReq());

const args = vi.mocked(connectSandbox).mock.calls[0]?.[0] as {
state: { source?: { repo: string; branch?: string; newBranch?: string } };
};
expect(args.state.source?.newBranch).toBe("xy/abcd1234");
expect(args.state.source?.branch).toBeUndefined();
});

it("omits both branch and newBranch when neither is provided (current default)", async () => {
await createSandboxHandler(makeReq());

const args = vi.mocked(connectSandbox).mock.calls[0]?.[0] as {
state: { source?: { repo: string; branch?: string; newBranch?: string } };
};
expect(args.state.source?.branch).toBeUndefined();
expect(args.state.source?.newBranch).toBeUndefined();
});

it("short-circuits with the validator's response on validation failure", async () => {
const fail = NextResponse.json({ status: "error", error: "bad" }, { status: 400 });
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce(fail);
Expand Down
19 changes: 17 additions & 2 deletions lib/sandbox/__tests__/validateCreateSandboxBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,31 @@ describe("validateCreateSandboxBody", () => {
expect(result.body.sessionId).toBe("sess-1");
});

it("strips an unknown branch input from the validated body", async () => {
it("preserves `branch` and `isNewBranch` on the validated body", async () => {
const result = await validateCreateSandboxBody(
makeReq({
repoUrl: "https://github.com/o/r",
branch: "feat/x",
isNewBranch: true,
}),
);

expect(result).not.toBeInstanceOf(NextResponse);
if (result instanceof NextResponse) return;
expect((result.body as Record<string, unknown>).branch).toBeUndefined();
expect(result.body.branch).toBe("feat/x");
expect(result.body.isNewBranch).toBe(true);
});

it("rejects a non-boolean isNewBranch", async () => {
const result = await validateCreateSandboxBody(
makeReq({
repoUrl: "https://github.com/o/r",
isNewBranch: "yes",
}),
);

expect(result).toBeInstanceOf(NextResponse);
if (!(result instanceof NextResponse)) return;
expect(result.status).toBe(400);
});
});
12 changes: 11 additions & 1 deletion lib/sandbox/createSandboxHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,17 @@ export async function createSandboxHandler(request: NextRequest): Promise<NextRe
// the snapshot. Without this flag, Vercel treats the snapshot as a
// base image and tries to clone fresh on top — which often fails
// for private repos and definitely defeats the warm-boot benefit.
source: { repo: body.repoUrl, prebuilt: !!orgSnapshotId },
//
// Branch routing: `isNewBranch` flips the body's `branch` between
// "check out this existing ref" (`branch`) and "cut a fresh ref
// off default" (`newBranch`). When neither is set, the runtime
// resolves to the repo's default branch.
source: {
repo: body.repoUrl,
...(body.branch && !body.isNewBranch ? { branch: body.branch } : {}),
...(body.branch && body.isNewBranch ? { newBranch: body.branch } : {}),
prebuilt: !!orgSnapshotId,
},
},
options: {
timeout: DEFAULT_TIMEOUT_MS,
Expand Down
8 changes: 8 additions & 0 deletions lib/sandbox/validateCreateSandboxBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ export const createSandboxBodySchema = z.object({
message: "repoUrl must be a valid GitHub repository URL",
}),
sessionId: z.string().optional(),
// Branch routing matches the open-agents contract: when
// `isNewBranch` is false (or omitted), `branch` names an existing
// ref to check out; when `isNewBranch` is true, `branch` names the
// *new* ref to create off the repo's default. The handler passes
// these to `connectSandbox.state.source` as `branch` / `newBranch`
// respectively.
branch: z.string().optional(),
isNewBranch: z.boolean().optional(),
});

export type CreateSandboxBody = z.infer<typeof createSandboxBodySchema>;
Expand Down
Loading