Skip to content
Merged
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
36 changes: 36 additions & 0 deletions lib/recoupable/__tests__/extractOrgRepoName.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, it, expect } from "vitest";
import { extractOrgRepoName } from "@/lib/recoupable/extractOrgRepoName";

describe("extractOrgRepoName", () => {
it("extracts the repo name from a recoupable org clone URL", () => {
expect(extractOrgRepoName("https://github.com/recoupable/org-rostrum-pacific-abc123")).toBe(
"org-rostrum-pacific-abc123",
);
});

it("strips a trailing .git suffix", () => {
expect(extractOrgRepoName("https://github.com/recoupable/myorg.git")).toBe("myorg");
});

it("strips a trailing slash", () => {
expect(extractOrgRepoName("https://github.com/recoupable/myorg/")).toBe("myorg");
});

it("returns null for non-recoupable orgs", () => {
expect(extractOrgRepoName("https://github.com/someoneelse/repo")).toBeNull();
});

it("returns null for nested paths beyond the repo segment", () => {
expect(extractOrgRepoName("https://github.com/recoupable/repo/blob/main/x")).toBeNull();
});

it("returns null for non-GitHub URLs", () => {
expect(extractOrgRepoName("https://gitlab.com/recoupable/repo")).toBeNull();
expect(extractOrgRepoName("not-a-url")).toBeNull();
});

it("returns null for the org root with no repo", () => {
expect(extractOrgRepoName("https://github.com/recoupable/")).toBeNull();
expect(extractOrgRepoName("https://github.com/recoupable")).toBeNull();
});
});
19 changes: 19 additions & 0 deletions lib/recoupable/extractOrgRepoName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const ORG_REPO_URL_PATTERN = /^https:\/\/github\.com\/recoupable\/([^/]+?)(?:\.git)?\/?$/;

/**
* Extracts the repo name from a Recoupable org clone URL. The repo
* name is used as a `sandboxName` to look up per-org base snapshots
* built by the build-org-snapshot workflow — finding one warm-boots
* the sandbox in seconds instead of paying the ~75s full-clone path.
*
* Example: `https://github.com/recoupable/org-rostrum-pacific-<uuid>`
* → `org-rostrum-pacific-<uuid>`
*
* @param cloneUrl - The repo URL the caller wants to clone.
* @returns The repo name when the URL is under the recoupable org,
* otherwise null. Non-recoupable repos skip the snapshot lookup.
*/
export function extractOrgRepoName(cloneUrl: string): string | null {
const match = cloneUrl.match(ORG_REPO_URL_PATTERN);
return match?.[1] ?? null;
}
63 changes: 63 additions & 0 deletions lib/sandbox/__tests__/createSandboxHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { selectSessions } from "@/lib/supabase/sessions/selectSessions";
import { connectSandbox } from "@/lib/sandbox/factory";
import { updateSession } from "@/lib/supabase/sessions/updateSession";
import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills";
import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot";

vi.mock("@/lib/networking/getCorsHeaders", () => ({
getCorsHeaders: () => ({ "Access-Control-Allow-Origin": "*" }),
Expand All @@ -29,6 +30,9 @@ vi.mock("@/lib/github/getServiceGithubToken", () => ({
vi.mock("@/lib/sandbox/installSessionGlobalSkills", () => ({
installSessionGlobalSkills: vi.fn(async () => undefined),
}));
vi.mock("@/lib/sandbox/findOrgSnapshot", () => ({
findOrgSnapshot: vi.fn(async () => null),
}));

const ACCOUNT_ID = "acc-1";

Expand Down Expand Up @@ -167,6 +171,65 @@ describe("createSandboxHandler", () => {
expect(res.status).toBe(200);
});

it("looks up an org snapshot and plumbs its id into baseSnapshotId when the repo is a recoupable org repo", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: {
repoUrl: "https://github.com/recoupable/org-acme-xyz",
sessionId: "sess-1",
},
auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" },
});
vi.mocked(findOrgSnapshot).mockResolvedValueOnce("snap_abc123");

await createSandboxHandler(makeReq());

expect(findOrgSnapshot).toHaveBeenCalledWith("org-acme-xyz");
const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0];
if (!arg || !("options" in arg)) throw new Error("expected new-API config shape");
if (!("state" in arg)) throw new Error("expected new-API state shape");
expect(arg.options?.baseSnapshotId).toBe("snap_abc123");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((arg.state as any).source.prebuilt).toBe(true);
});

it("skips the snapshot lookup entirely for non-recoupable repos", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: {
repoUrl: "https://github.com/someoneelse/repo",
sessionId: "sess-1",
},
auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" },
});

await createSandboxHandler(makeReq());

expect(findOrgSnapshot).not.toHaveBeenCalled();
const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0];
if (!arg || !("options" in arg)) throw new Error("expected new-API config shape");
expect(arg.options?.baseSnapshotId).toBeUndefined();
});

it("does not pass baseSnapshotId when the org snapshot lookup misses", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: {
repoUrl: "https://github.com/recoupable/org-no-snap-yet",
sessionId: "sess-1",
},
auth: { accountId: ACCOUNT_ID, orgId: null, authToken: "k" },
});
vi.mocked(findOrgSnapshot).mockResolvedValueOnce(null);

await createSandboxHandler(makeReq());

expect(findOrgSnapshot).toHaveBeenCalledWith("org-no-snap-yet");
const arg = vi.mocked(connectSandbox).mock.calls[0]?.[0];
if (!arg || !("options" in arg)) throw new Error("expected new-API config shape");
if (!("state" in arg)) throw new Error("expected new-API state shape");
expect(arg.options?.baseSnapshotId).toBeUndefined();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((arg.state as any).source.prebuilt).toBe(false);
});

it("does not attempt skill installation when no sessionId is provided", async () => {
vi.mocked(validateCreateSandboxBody).mockResolvedValueOnce({
body: { repoUrl: "https://github.com/o/r" },
Expand Down
55 changes: 55 additions & 0 deletions lib/sandbox/__tests__/findOrgSnapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot";
import { Snapshot } from "@vercel/sandbox";

vi.mock("@vercel/sandbox", () => ({
Snapshot: { list: vi.fn() },
}));

beforeEach(() => {
vi.clearAllMocks();
});

describe("findOrgSnapshot", () => {
it("returns the id of the most recent created snapshot", async () => {
vi.mocked(Snapshot.list).mockResolvedValue({
snapshots: [
{ id: "snap_A", status: "creating" },
{ id: "snap_B", status: "created" },
{ id: "snap_C", status: "created" },
],
} as never);

const id = await findOrgSnapshot("org-x");
// Returns the FIRST created snapshot in the list (sortOrder desc means first = newest).
expect(id).toBe("snap_B");
});

it("calls Snapshot.list with the supplied name and a desc sort order", async () => {
vi.mocked(Snapshot.list).mockResolvedValue({ snapshots: [] } as never);

await findOrgSnapshot("org-y");

expect(Snapshot.list).toHaveBeenCalledWith(
expect.objectContaining({ name: "org-y", sortOrder: "desc" }),
);
});

it("returns null when no snapshots are in the 'created' state", async () => {
vi.mocked(Snapshot.list).mockResolvedValue({
snapshots: [{ id: "snap_pending", status: "creating" }],
} as never);

expect(await findOrgSnapshot("org-z")).toBeNull();
});

it("returns null when the API returns no snapshots", async () => {
vi.mocked(Snapshot.list).mockResolvedValue({ snapshots: [] } as never);
expect(await findOrgSnapshot("org-empty")).toBeNull();
});

it("returns null when Snapshot.list throws", async () => {
vi.mocked(Snapshot.list).mockRejectedValue(new Error("vercel api down"));
expect(await findOrgSnapshot("org-err")).toBeNull();
});
});
19 changes: 18 additions & 1 deletion lib/sandbox/createSandboxHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
import { validateCreateSandboxBody } from "@/lib/sandbox/validateCreateSandboxBody";
import { selectSessions } from "@/lib/supabase/sessions/selectSessions";
import { connectSandbox } from "@/lib/sandbox/factory";
import { findOrgSnapshot } from "@/lib/sandbox/findOrgSnapshot";
import { getSessionSandboxName } from "@/lib/sandbox/getSessionSandboxName";
import { installSessionGlobalSkills } from "@/lib/sandbox/installSessionGlobalSkills";
import { extractOrgRepoName } from "@/lib/recoupable/extractOrgRepoName";
import { updateSession } from "@/lib/supabase/sessions/updateSession";
import { getServiceGithubToken } from "@/lib/github/getServiceGithubToken";
import type { Json, Tables } from "@/types/database.types";
Expand Down Expand Up @@ -57,6 +59,14 @@ export async function createSandboxHandler(request: NextRequest): Promise<NextRe
}

const sandboxName = sessionId ? getSessionSandboxName(sessionId) : undefined;

// Per-org base snapshot lookup — saves ~75s on cold start when found.
// Skipped entirely for non-recoupable repos so this only costs latency
// for the case where it can pay off. A miss falls through to default
// sandbox provisioning; an error is logged and treated as a miss.
const orgRepoName = extractOrgRepoName(body.repoUrl);
const orgSnapshotId = orgRepoName ? await findOrgSnapshot(orgRepoName) : null;

const startTime = Date.now();

let sandbox;
Expand All @@ -65,12 +75,19 @@ export async function createSandboxHandler(request: NextRequest): Promise<NextRe
state: {
type: "vercel",
...(sandboxName ? { sandboxName } : {}),
source: { repo: body.repoUrl },
// `prebuilt: true` when restoring from an org snapshot tells the
// Vercel sandbox runtime to skip the fresh `git clone` and instead
// `git fetch` + `git reset --hard` the repo that's already inside
// 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 },
},
options: {
timeout: DEFAULT_TIMEOUT_MS,
ports: DEFAULT_PORTS,
githubToken: getServiceGithubToken(),
...(orgSnapshotId ? { baseSnapshotId: orgSnapshotId } : {}),
persistent: !!sandboxName,
resume: !!sandboxName,
createIfMissing: !!sandboxName,
Expand Down
32 changes: 32 additions & 0 deletions lib/sandbox/findOrgSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Snapshot } from "@vercel/sandbox";

/**
* Looks up the most recent ready snapshot whose source sandbox name
* matches `sandboxName`. Used by `POST /api/sandbox` to warm-boot
* recoupable org sessions from a per-org base snapshot when one
* exists, instead of paying the full-clone cold-start path.
*
* Returns the snapshot id, or `null` when no `created` snapshot
* exists yet or when the upstream call throws — callers always have
* a safe fallback to default sandbox provisioning.
*
* @param sandboxName - The org repo name returned by `extractOrgRepoName`.
* @returns The snapshot id, or null on miss / error.
*/
export async function findOrgSnapshot(sandboxName: string): Promise<string | null> {
try {
const result = await Snapshot.list({
name: sandboxName,
sortOrder: "desc",
limit: 5,
});
const ready = result.snapshots.find(s => s.status === "created");
console.log(
`[findOrgSnapshot] '${sandboxName}' → ${ready ? `hit ${ready.id}` : "miss"} (${result.snapshots.length} total snapshots returned)`,
);
return ready?.id ?? null;
} catch (error) {
console.error(`[findOrgSnapshot] failed to list snapshots for '${sandboxName}':`, error);
return null;
}
}
Loading