feat(sandbox): port GET /api/sandbox/reconnect from open-agents#525
Conversation
Implements the live runtime probe endpoint required for the chat loading-UX cutover on session re-entry / tab refocus. Matches the contract documented in recoupable/docs#195 (now merged on main). Unlike GET /api/sandbox/status (DB-only read), /reconnect actually runs `sandbox.exec("pwd")` inside the runtime so the UI can distinguish a truly-alive sandbox from a DB row whose sandbox no longer exists. Behavior: - 200 status="no_sandbox" when sandbox_state lacks runtime metadata (delegates to hasRuntimeSandboxState — the same gate /status uses) - 200 status="connected" with sandbox.expiresAt when the probe succeeds - 200 status="expired" when the probe throws — also clears sandbox_state on the session row and sets lifecycle_state to "hibernated" so subsequent /status reads agree with the probe - hasSnapshot derived from snapshot_url across all three outcomes - 4xx for auth (401), missing sessionId (400), forbidden (403), not-found (404) — same envelope as /status Files follow the existing api conventions established by PR #522: - app/api/sandbox/reconnect/route.ts: thin GET delegation + OPTIONS - lib/sandbox/getSandboxReconnectHandler.ts: handler logic - Reuses validateAuthContext, selectSessions, hasRuntimeSandboxState, buildLifecycle, connectSandbox, updateSession - Error envelope { status, error } matches sessions PRs TDD red -> green: - Handler tests cover auth fail, missing sessionId, 404, 403, no_sandbox, hasSnapshot derivation, connected (with expiresAt), expired (with state-clear assertion), and lifecycle envelope shape - Thin route shell test asserts delegation - Suite: 2516 -> 2526 (+10 net new tests), pnpm lint:check clean Out of scope (deferred per the gap analysis): - Transient vs unavailable error distinction. open-agents preserves runtime state on transient errors (network blip != dead sandbox). v1 treats every probe failure as expired, which is safer for the loading UX (user can retry). Worth porting once we have a real signal that this is happening in production. - Stale-state lifecycle workflow kick (open-agents' /status has it too — needs workflow infra in api). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
To continue reviewing without waiting, purchase usage credits in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR introduces a new ChangesSandbox Reconnect Endpoint
Estimated Code Review Effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
lib/sandbox/getSandboxReconnectHandler.ts (3)
99-109: ⚡ Quick winInline lifecycle construction in the expired branch is a DRY risk.
The
"no_sandbox"path callsbuildLifecycle(row), but the"expired"branch hand-rolls the same shape. WhileReturnType<typeof buildLifecycle>will cause a compile error if the shapes diverge today, any new semantically meaningful field added tobuildLifecyclein the future won't automatically appear here.A cleaner alternative is to fabricate an updated row and delegate to
buildLifecycle:♻️ Proposed refactor
- const body: ReconnectBody = { - status: "expired", - hasSnapshot: !!row.snapshot_url, - lifecycle: { - serverTime: Date.now(), - state: "hibernated", - lastActivityAt: null, - hibernateAfter: null, - sandboxExpiresAt: null, - }, - }; + const expiredRow = { + ...row, + lifecycle_state: "hibernated" as const, + sandbox_state: null, + sandbox_expires_at: null, + }; + const body: ReconnectBody = { + status: "expired", + hasSnapshot: !!row.snapshot_url, + lifecycle: buildLifecycle(expiredRow), + };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/sandbox/getSandboxReconnectHandler.ts` around lines 99 - 109, The expired branch manually constructs a lifecycle object, duplicating the shape produced by buildLifecycle and risking future drift; instead, create an updated row object that reflects the expired state (e.g., set fields like state: "hibernated", lastActivityAt: null, hibernateAfter: null, sandboxExpiresAt: null or adjust snapshot_url as needed) and call buildLifecycle(updatedRow) to produce the lifecycle used in the ReconnectBody (replace the inline lifecycle in the expired branch with the result of buildLifecycle), ensuring you still set status: "expired" and hasSnapshot: !!row.snapshot_url on the body.
42-112: ⚖️ Poor tradeoffHandler exceeds the 50-line guideline for
lib/functions.
getSandboxReconnectHandlerspans ~70 lines. The auth guard, ownership check, short-circuit, live probe, and expiry recovery are distinct responsibilities. Consider extracting the live-probe logic (lines 77–111) into a focused helper (e.g.,probeSandboxConnection) to bring the main handler under the limit and improve unit-testability of each path.As per coding guidelines: "Keep functions under 50 lines" and "Flag functions longer than 20 lines."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/sandbox/getSandboxReconnectHandler.ts` around lines 42 - 112, getSandboxReconnectHandler is over the 50-line limit; extract the live-probe and expiry-recovery logic into a new helper (e.g., probeSandboxConnection) so the handler only performs auth/ownership checks and delegates probing. The helper should accept the session row and sessionId, call connectSandbox(row.sandbox_state as SandboxState), run sandbox.exec("pwd", sandbox.workingDirectory, PROBE_TIMEOUT_MS), return the ReconnectBody on success (using sandbox.expiresAt and buildLifecycle(row)), and on failure perform the updateSession(...) rollback (setting sandbox_state/null, lifecycle_state "hibernated", sandbox_expires_at null, hibernate_after null) and return the expired ReconnectBody; then make getSandboxReconnectHandler call probeSandboxConnection and return its NextResponse (or construct the JSON response from the helper result), keeping references to connectSandbox, PROBE_TIMEOUT_MS, updateSession, noSandboxResponse, selectSessions, and buildLifecycle.
48-54: ⚡ Quick win
sessionIdneeds Zod schema validation.The query param is validated only for null/absence. A Zod schema would catch malformed values (e.g., non-UUID strings) before they hit the database, consistent with the project's validation standards.
✨ Proposed Zod-based validation
+import { z } from "zod"; + +const ReconnectQuerySchema = z.object({ + sessionId: z.string().uuid("sessionId must be a valid UUID"), +}); - const sessionId = request.nextUrl.searchParams.get("sessionId"); - if (!sessionId) { + const parseResult = ReconnectQuerySchema.safeParse({ + sessionId: request.nextUrl.searchParams.get("sessionId"), + }); + if (!parseResult.success) { return NextResponse.json( - { status: "error", error: "Missing sessionId" }, + { status: "error", error: parseResult.error.issues[0].message }, { status: 400, headers: getCorsHeaders() }, ); } + const { sessionId } = parseResult.data;As per coding guidelines: "All API endpoints should use a validate function for input parsing using Zod for schema validation."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/sandbox/getSandboxReconnectHandler.ts` around lines 48 - 54, The handler currently only checks for missing sessionId but lacks Zod validation; update the request parsing in getSandboxReconnectHandler (where sessionId is read from request.nextUrl.searchParams.get) to validate the query param with a Zod schema (e.g., z.string().uuid() or the project's shared validate utility) before using it; if validation fails return the same NextResponse.json error pattern with status 400 and getCorsHeaders(), otherwise proceed with the validated sessionId value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/sandbox/getSandboxReconnectHandler.ts`:
- Around line 92-97: The updateSession call is clearing the user's
hibernate_after preference (setting hibernate_after: null) which violates the
described expired-path contract and is inconsistent with the no_sandbox path
that preserves hibernateAfter via buildLifecycle(row); remove the
hibernate_after: null assignment (or explicitly preserve the existing value by
reading row.hibernate_after or using buildLifecycle(row) to compute lifecycle)
so expiry only clears sandbox_state and sets lifecycle_state to "hibernated"
without wiping user preferences in updateSession.
---
Nitpick comments:
In `@lib/sandbox/getSandboxReconnectHandler.ts`:
- Around line 99-109: The expired branch manually constructs a lifecycle object,
duplicating the shape produced by buildLifecycle and risking future drift;
instead, create an updated row object that reflects the expired state (e.g., set
fields like state: "hibernated", lastActivityAt: null, hibernateAfter: null,
sandboxExpiresAt: null or adjust snapshot_url as needed) and call
buildLifecycle(updatedRow) to produce the lifecycle used in the ReconnectBody
(replace the inline lifecycle in the expired branch with the result of
buildLifecycle), ensuring you still set status: "expired" and hasSnapshot:
!!row.snapshot_url on the body.
- Around line 42-112: getSandboxReconnectHandler is over the 50-line limit;
extract the live-probe and expiry-recovery logic into a new helper (e.g.,
probeSandboxConnection) so the handler only performs auth/ownership checks and
delegates probing. The helper should accept the session row and sessionId, call
connectSandbox(row.sandbox_state as SandboxState), run sandbox.exec("pwd",
sandbox.workingDirectory, PROBE_TIMEOUT_MS), return the ReconnectBody on success
(using sandbox.expiresAt and buildLifecycle(row)), and on failure perform the
updateSession(...) rollback (setting sandbox_state/null, lifecycle_state
"hibernated", sandbox_expires_at null, hibernate_after null) and return the
expired ReconnectBody; then make getSandboxReconnectHandler call
probeSandboxConnection and return its NextResponse (or construct the JSON
response from the helper result), keeping references to connectSandbox,
PROBE_TIMEOUT_MS, updateSession, noSandboxResponse, selectSessions, and
buildLifecycle.
- Around line 48-54: The handler currently only checks for missing sessionId but
lacks Zod validation; update the request parsing in getSandboxReconnectHandler
(where sessionId is read from request.nextUrl.searchParams.get) to validate the
query param with a Zod schema (e.g., z.string().uuid() or the project's shared
validate utility) before using it; if validation fails return the same
NextResponse.json error pattern with status 400 and getCorsHeaders(), otherwise
proceed with the validated sessionId value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e93c3680-dc46-4803-82b1-b012ca97e4aa
⛔ Files ignored due to path filters (2)
app/api/sandbox/reconnect/__tests__/route.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included byapp/**lib/sandbox/__tests__/getSandboxReconnectHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (2)
app/api/sandbox/reconnect/route.tslib/sandbox/getSandboxReconnectHandler.ts
There was a problem hiding this comment.
3 issues found across 4 files
Confidence score: 3/5
- There is a moderate merge risk:
lib/sandbox/getSandboxReconnectHandler.tshas a high-confidence API contract concern (severity 6/10) where a GET reconnect path mutates persisted session state, which can cause inconsistent behavior for clients expecting retrieval-only semantics. - The remaining findings are maintainability-focused (line-limit/style) in
lib/sandbox/__tests__/getSandboxReconnectHandler.test.tsandlib/sandbox/getSandboxReconnectHandler.ts; these are less likely to break runtime behavior but can make future changes and reviews harder. - Pay close attention to
lib/sandbox/getSandboxReconnectHandler.ts,lib/sandbox/__tests__/getSandboxReconnectHandler.test.ts- resolve GET-side mutation semantics first, then split/trim oversized modules for maintainability.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/sandbox/__tests__/getSandboxReconnectHandler.test.ts">
<violation number="1" location="lib/sandbox/__tests__/getSandboxReconnectHandler.test.ts:1">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**
Test file exceeds the repository's under-100-line limit and combines too many concerns in one module.</violation>
</file>
<file name="lib/sandbox/getSandboxReconnectHandler.ts">
<violation number="1" location="lib/sandbox/getSandboxReconnectHandler.ts:1">
P3: Custom agent: **Enforce Clear Code Style and Maintainability Practices**
New file exceeds the custom under-100-lines limit.</violation>
<violation number="2" location="lib/sandbox/getSandboxReconnectHandler.ts:92">
P2: Custom agent: **API Design Consistency and Maintainability**
GET endpoint performs a persisted session mutation on the reconnect failure path, so this route is not retrieval-only as required by the API method rule.</violation>
</file>
Architecture diagram
sequenceDiagram
participant UI as "Chat UI (Browser)"
participant Route as "GET /api/sandbox/reconnect"
participant Handler as "getSandboxReconnectHandler"
participant Auth as "validateAuthContext"
participant DB as "Supabase (Sessions)"
participant Runtime as "Sandbox Runtime (Vercel)"
Note over UI,Route: Session re-entry / tab refocus
UI->>Route: GET /api/sandbox/reconnect?sessionId=xxx
Route->>Handler: Delegate request
Handler->>Auth: Validate auth context
Auth-->>Handler: { accountId, orgId, authToken }
alt 401 - Auth fails
Handler-->>Route: { status: "error", error: "Unauthorized" }
Route-->>UI: 401
else 400 - Missing sessionId
Handler-->>Route: { status: "error", error: "Missing sessionId" }
Route-->>UI: 400
else Auth succeeds
Handler->>DB: selectSessions({ id: sessionId })
DB-->>Handler: Session row
alt 404 - Session not found
Handler-->>Route: { status: "error", error: "Session not found" }
Route-->>UI: 404
else 403 - Not owned by account
Handler-->>Route: { status: "error", error: "Forbidden" }
Route-->>UI: 403
else Session found & owned
alt hasRuntimeSandboxState(row.sandbox_state) == false
Note over Handler: Short-circuit - no runtime metadata
Handler-->>Route: { status: "no_sandbox", hasSnapshot, lifecycle }
Route-->>UI: 200 no_sandbox
else Runtime metadata exists
Handler->>Runtime: connectSandbox(runtime state)
Runtime-->>Handler: Sandbox handle
alt Probe succeeds
Handler->>Runtime: sandbox.exec("pwd", ...timeout=15s)
Runtime-->>Handler: { success: true, stdout: "/workspace" }
Handler-->>Route: { status: "connected", hasSnapshot, expiresAt, lifecycle }
Route-->>UI: 200 connected
else Probe failure
Note over Handler: Runtime unreachable - clear stale state
Handler->>DB: updateSession(sessionId, { sandbox_state: null, lifecycle_state: "hibernated", ... })
Handler-->>Route: { status: "expired", hasSnapshot, lifecycle }
Route-->>UI: 200 expired
end
end
end
end
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| @@ -0,0 +1,177 @@ | |||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | |||
There was a problem hiding this comment.
P2: Custom agent: Enforce Clear Code Style and Maintainability Practices
Test file exceeds the repository's under-100-line limit and combines too many concerns in one module.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/__tests__/getSandboxReconnectHandler.test.ts, line 1:
<comment>Test file exceeds the repository's under-100-line limit and combines too many concerns in one module.</comment>
<file context>
@@ -0,0 +1,177 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { NextRequest, NextResponse } from "next/server";
+
</file context>
| const message = error instanceof Error ? error.message : String(error); | ||
| console.warn(`[getSandboxReconnectHandler] probe failed for ${sessionId}: ${message}`); | ||
|
|
||
| await updateSession(sessionId, { |
There was a problem hiding this comment.
P2: Custom agent: API Design Consistency and Maintainability
GET endpoint performs a persisted session mutation on the reconnect failure path, so this route is not retrieval-only as required by the API method rule.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/sandbox/getSandboxReconnectHandler.ts, line 92:
<comment>GET endpoint performs a persisted session mutation on the reconnect failure path, so this route is not retrieval-only as required by the API method rule.</comment>
<file context>
@@ -0,0 +1,112 @@
+ const message = error instanceof Error ? error.message : String(error);
+ console.warn(`[getSandboxReconnectHandler] probe failed for ${sessionId}: ${message}`);
+
+ await updateSession(sessionId, {
+ sandbox_state: null,
+ lifecycle_state: "hibernated",
</file context>
| lifecycle: ReturnType<typeof buildLifecycle>; | ||
| } | ||
|
|
||
| function noSandboxResponse(row: Tables<"sessions">): NextResponse { |
There was a problem hiding this comment.
SRP - new lib for noSandboxResponse.ts
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>
|
Pushed Extracted TDD red → green confirmed; suite 2526 → 2529, lint clean. |
| const auth = await validateAuthContext(request); | ||
| if (auth instanceof NextResponse) { | ||
| return auth; | ||
| } | ||
|
|
||
| const sessionId = request.nextUrl.searchParams.get("sessionId"); | ||
| if (!sessionId) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Missing sessionId" }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| const rows = await selectSessions({ id: sessionId }); | ||
| const row = rows[0]; | ||
|
|
||
| if (!row) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Session not found" }, | ||
| { status: 404, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| if (row.account_id !== auth.accountId) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Forbidden" }, | ||
| { status: 403, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| if (!hasRuntimeSandboxState(row.sandbox_state)) { | ||
| return noSandboxResponse(row); | ||
| } |
There was a problem hiding this comment.
SRP
- actual: validate logic defined in the handler
- required: new lib for the validate logic.
…iew build
Two changes bundled:
1) SRP per review feedback (sweetmantech) — extract the auth +
sessionId-from-query + session lookup + ownership check pre-flight
from `getSandboxReconnectHandler` into its own
`validateSandboxReconnectRequest.ts`. Mirrors the
`validateCreateSandboxBody` pattern: returns either a 4xx
NextResponse describing the first failure, or `{ row, auth }` for
the handler to consume.
2) Type fix for `next build` — `connectSandbox(row.sandbox_state as
SandboxState)` failed to compile against `Json` (union includes
primitives + arrays); cast through `unknown` first. The
`hasRuntimeSandboxState` gate above ensures the runtime shape is
safe at the call site, so the double cast is justified — comment
added explaining why.
The vitest pass alone wasn't enough to catch the type error — `next
build` runs a separate `tsc` step that the test runner skips. Caught
by the Vercel preview build failing on the previous commit.
TDD red -> green:
- 5 unit tests for the new validator covering auth fail (passes
through), missing sessionId (400), session not found (404),
ownership mismatch (403), and happy-path return shape ({row, auth})
- Existing handler tests pass unchanged — the module-level mocks for
validateAuthContext / selectSessions still intercept the calls now
that they're behind the new validator
- Suite: 2529 -> 2534, pnpm lint:check clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed SRP (r3203391529): Extracted the pre-flight validate logic (auth + sessionId-from-query + session lookup + ownership) from Type-fix bonus: the previous two preview deployments errored at the
TDD red → green throughout. 5 unit tests for the new validator. Existing handler tests pass unchanged (their |
Smoke test results — preview deploymentRan end-to-end against `https://api-git-feat-sandbox-reconnect-recoup.vercel.app\` after the build fix landed in `8a1e660d`. All paths green. ✅ Negative paths
✅ Operational paths
HTTP 200
{
"status": "no_sandbox",
"hasSnapshot": false,
"lifecycle": { "serverTime": ..., "state": "provisioning", ... }
}The `hasRuntimeSandboxState` gate correctly skips the live probe for the type-stub written by `POST /api/sessions`.
HTTP 200
{
"status": "connected",
"hasSnapshot": false,
"expiresAt": 1778177756888,
"lifecycle": {
"serverTime": 1778175957909,
"state": "active",
"lastActivityAt": 1778175956634,
"sandboxExpiresAt": 1778177756634
}
}Worth noting: top-level
|
Summary
Implements the live runtime probe endpoint needed for the chat loading-UX cutover on session re-entry / tab refocus. Matches the contract documented in recoupable/docs#195 (merged on main as
c44dea0).Sequel to #522 (POST
/api/sandbox+ GET/api/sandbox/status). Reuses everything that PR established —validateAuthContext,selectSessions,hasRuntimeSandboxState,buildLifecycle,connectSandbox,updateSession.Why this endpoint
GET /api/sandbox/statusis DB-only. It can't tell you whether the recordedsandbox_stateactually points at a sandbox that's still alive — only what the row says. On session re-entry (user closes the tab, comes back tomorrow) the chat UI needs to distinguish three cases:connected— sandbox is still reachable, resume immediatelyexpired— DB says we have one but the runtime is gone — UI should offer resume-from-snapshot or fresh-createno_sandbox— never had one/reconnectruns a quicksandbox.exec(\"pwd\")inside the sandbox to make that distinction. Without this endpoint, returning to a hibernated session leaves the UI stuck in a loading-state polling loop.Architecture
app/api/sandbox/reconnect/route.tslib/sandbox/getSandboxReconnectHandler.tsBehavior
\"no_sandbox\"whensandbox_statelacks runtime metadata (hasRuntimeSandboxStatereturns false). Skips the probe entirely.\"connected\"withsandbox.expiresAtwhen the probe succeeds.\"expired\"when the probe throws. Also clearssandbox_stateon the session row and setslifecycle_state: \"hibernated\"so subsequent/statusreads agree with the probe.hasSnapshotderived fromsnapshot_urlacross all three outcomes.sessionId(400), forbidden (403), not-found (404) — same{status, error}envelope as the existing endpoints.TDD
Strict red → green. +10 new tests, all passing (suite: 2516 → 2526):
lib/sandbox/__tests__/getSandboxReconnectHandler.test.tsapp/api/sandbox/reconnect/__tests__/route.test.tsCoverage: auth short-circuit, all 4xx response codes, no-runtime short-circuit (asserts probe is skipped), hasSnapshot derivation, connected (with
expiresAtfrom sandbox handle), expired (with state-clear assertion on the session row), lifecycle envelope shape on every 200.Verification
pnpm testpnpm lint:checkOut of scope (deferred)
sandboxName). Worth porting once we have a real signal this is happening in production./statushas the same gap — needs workflow infra in api.Test plan
GET /api/sandbox/reconnect?sessionId=…with no auth returns 401sessionIdreturns 400status: \"no_sandbox\"POST /api/sandboxprovisions,/reconnectreturns 200 withstatus: \"connected\"and a realexpiresAtWhat follows
Once this lands and the test → main promote happens:
/api/sandbox/reconnectcalls atRECOUPABLE_API_BASE_URLand deleting the local handler/status+ GET/reconnectbecomes safe (third missing piece resolved)🤖 Generated with Claude Code
Summary by cubic
Adds
GET /api/sandbox/reconnectto probe sandbox liveness on session re-entry so the UI can resume or recreate reliably. Extracts shared helpers and fixes anext buildtype error.New Features
pwd; returns 200 withstatus: "connected"(withexpiresAt),"expired"(clearssandbox_state, setslifecycle_state: "hibernated"), or"no_sandbox".hasSnapshotand a lifecycle envelope; 4xx for auth (401), missingsessionId(400), forbidden (403), and not-found (404).app/api/sandbox/reconnect/route.ts(with CORSOPTIONS); handler inlib/sandbox/getSandboxReconnectHandler.ts.Refactors
lib/sandbox/noSandboxResponse.tsandlib/sandbox/validateSandboxReconnectRequest.ts(auth +sessionId+ lookup + ownership).next buildtype issue by castingrow.sandbox_stateviaunknownbeforeSandboxState.no_sandboxhelper, and connected/expired flows.Written for commit 8a1e660. Summary will update on new commits.
Summary by CodeRabbit