Skip to content

Commit 23043f1

Browse files
sweetmantechclaude
andcommitted
refactor(sandbox): extract noSandboxResponse to its own file (SRP)
Per review feedback on PR #525 — pulls the inline `noSandboxResponse` helper out of `getSandboxReconnectHandler.ts` into its own file so it can be reused by future endpoints (e.g., `/snapshot` resume) and so the handler file stops carrying response-shape construction logic. The narrowed `ReconnectBody` type in the handler now only covers the two outcomes the handler actually constructs locally (`connected` / `expired`); the `no_sandbox` shape lives with its builder. TDD red -> green: 3 unit tests for the extracted helper covering 200 status, hasSnapshot derivation, and lifecycle envelope projection. Suite: 2526 -> 2529. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c66f12f commit 23043f1

3 files changed

Lines changed: 79 additions & 11 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import{describe,it,expect,vi}from"vitest";
2+
import{NextResponse}from"next/server";
3+
import{noSandboxResponse}from"@/lib/sandbox/noSandboxResponse";
4+
5+
vi.mock("@/lib/networking/getCorsHeaders",()=>({
6+
getCorsHeaders: ()=>({"Access-Control-Allow-Origin": "*"}),
7+
}));
8+
9+
constbaseRow={
10+
id: "sess-1",
11+
account_id: "acc-1",
12+
sandbox_state: null,
13+
lifecycle_state: null,
14+
lifecycle_version: 0,
15+
sandbox_expires_at: null,
16+
hibernate_after: null,
17+
last_activity_at: null,
18+
snapshot_url: null,
19+
};
20+
21+
describe("noSandboxResponse",()=>{
22+
it("returns a 200 NextResponse with status='no_sandbox'",async()=>{
23+
constres=noSandboxResponse(baseRowasnever);
24+
25+
expect(res).toBeInstanceOf(NextResponse);
26+
expect(res.status).toBe(200);
27+
constbody=awaitres.json();
28+
expect(body.status).toBe("no_sandbox");
29+
});
30+
31+
it("derives hasSnapshot from snapshot_url presence",async()=>{
32+
constwithSnap=awaitnoSandboxResponse({
33+
...baseRow,
34+
snapshot_url: "snap://x",
35+
}asnever).json();
36+
constwithout=awaitnoSandboxResponse(baseRowasnever).json();
37+
38+
expect(withSnap.hasSnapshot).toBe(true);
39+
expect(without.hasSnapshot).toBe(false);
40+
});
41+
42+
it("includes the lifecycle envelope projected from the row",async()=>{
43+
constbody=awaitnoSandboxResponse(baseRowasnever).json();
44+
45+
expect(body.lifecycle).toMatchObject({
46+
serverTime: expect.any(Number),
47+
state: null,
48+
lastActivityAt: null,
49+
hibernateAfter: null,
50+
sandboxExpiresAt: null,
51+
});
52+
});
53+
});

‎lib/sandbox/getSandboxReconnectHandler.ts‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,20 @@ import { validateAuthContext } from "@/lib/auth/validateAuthContext";
44
import{buildLifecycle}from"@/lib/sandbox/buildLifecycle";
55
import{connectSandbox}from"@/lib/sandbox/factory";
66
import{hasRuntimeSandboxState}from"@/lib/sandbox/hasRuntimeSandboxState";
7+
import{noSandboxResponse}from"@/lib/sandbox/noSandboxResponse";
78
import{selectSessions}from"@/lib/supabase/sessions/selectSessions";
89
import{updateSession}from"@/lib/supabase/sessions/updateSession";
910
importtype{SandboxState}from"@/lib/sandbox/factory";
10-
importtype{Tables}from"@/types/database.types";
1111

1212
constPROBE_TIMEOUT_MS=15_000;
1313

1414
interfaceReconnectBody{
15-
status: "connected"|"expired"|"no_sandbox";
15+
status: "connected"|"expired";
1616
hasSnapshot: boolean;
1717
expiresAt?: number;
1818
lifecycle: ReturnType<typeofbuildLifecycle>;
1919
}
2020

21-
functionnoSandboxResponse(row: Tables<"sessions">): NextResponse{
22-
constbody: ReconnectBody={
23-
status: "no_sandbox",
24-
hasSnapshot: !!row.snapshot_url,
25-
lifecycle: buildLifecycle(row),
26-
};
27-
returnNextResponse.json(body,{status: 200,headers: getCorsHeaders()});
28-
}
29-
3021
/**
3122
* Handles `GET /api/sandbox/reconnect`. Live runtime probe — actually
3223
* runs a quick command inside the sandbox to verify it is reachable.

‎lib/sandbox/noSandboxResponse.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import{NextResponse}from"next/server";
2+
import{getCorsHeaders}from"@/lib/networking/getCorsHeaders";
3+
import{buildLifecycle}from"@/lib/sandbox/buildLifecycle";
4+
importtype{Tables}from"@/types/database.types";
5+
6+
/**
7+
* Builds the `status: "no_sandbox"` response shared by sandbox lifecycle
8+
* endpoints (currently `/reconnect`). Used when the session row lacks
9+
* runtime metadata — there is no live sandbox to probe, so report that
10+
* directly along with whether a snapshot exists for resume affordances.
11+
*
12+
* @param row - The `sessions` row.
13+
* @returns A 200 NextResponse with `{status, hasSnapshot, lifecycle}`.
14+
*/
15+
exportfunctionnoSandboxResponse(row: Tables<"sessions">): NextResponse{
16+
returnNextResponse.json(
17+
{
18+
status: "no_sandbox"asconst,
19+
hasSnapshot: !!row.snapshot_url,
20+
lifecycle: buildLifecycle(row),
21+
},
22+
{status: 200,headers: getCorsHeaders()},
23+
);
24+
}

0 commit comments

Comments
 (0)