diff --git a/src/features/dashboard/sandbox/terminal/view.tsx b/src/features/dashboard/sandbox/terminal/view.tsx
index 107c51a75..4b7292377 100644
--- a/src/features/dashboard/sandbox/terminal/view.tsx
+++ b/src/features/dashboard/sandbox/terminal/view.tsx
@@ -1,7 +1,6 @@
'use client'
import { type ReactNode, useMemo, useState } from 'react'
-import type { SandboxManagementAuth } from '@/core/shared/sandbox-management-auth'
import LoadingLayout from '@/features/dashboard/loading-layout'
import DashboardTerminal from '@/features/dashboard/terminal/dashboard-terminal'
import { useRouteParams } from '@/lib/hooks/use-route-params'
@@ -11,14 +10,14 @@ import SandboxInspectNotFound from '../inspect/not-found'
interface SandboxTerminalViewProps {
command?: string
- sandboxManagementAuth: SandboxManagementAuth
+ userId: string
}
const SANDBOX_TERMINAL_RESUME_TIMEOUT_MS = 70_000
export default function SandboxTerminalView({
command,
- sandboxManagementAuth,
+ userId,
}: SandboxTerminalViewProps) {
const [shouldResumeSandbox, setShouldResumeSandbox] = useState(false)
const [terminalResumeError, setTerminalResumeError] = useState()
@@ -96,9 +95,9 @@ export default function SandboxTerminalView({
sandboxConnectRequestTimeoutMs={
shouldResumeSandbox ? SANDBOX_TERMINAL_RESUME_TIMEOUT_MS : undefined
}
- sandboxManagementAuth={sandboxManagementAuth}
sandboxScoped
teamSlug={team.slug}
+ userId={userId}
/>
)
diff --git a/src/features/dashboard/terminal/dashboard-terminal.tsx b/src/features/dashboard/terminal/dashboard-terminal.tsx
index c15872ebd..c3c443193 100644
--- a/src/features/dashboard/terminal/dashboard-terminal.tsx
+++ b/src/features/dashboard/terminal/dashboard-terminal.tsx
@@ -2,7 +2,7 @@
import type { CommandHandle, Sandbox } from 'e2b'
import { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react'
-import type { SandboxManagementAuth } from '@/core/shared/sandbox-management-auth'
+import { useTRPCClient } from '@/trpc/client'
import {
DEFAULT_CWD,
TERMINAL_ATTACH_ATTEMPT_TIMEOUT_MS,
@@ -35,9 +35,9 @@ interface DashboardTerminalProps {
onSandboxAttached?: (sandboxId: string) => void
onSandboxAttachFailed?: (target: TerminalLaunchTarget | undefined) => void
sandboxConnectRequestTimeoutMs?: number
- sandboxManagementAuth: SandboxManagementAuth
sandboxScoped?: boolean
teamSlug: string
+ userId: string
}
export default function DashboardTerminal({
@@ -47,10 +47,12 @@ export default function DashboardTerminal({
onSandboxAttached,
onSandboxAttachFailed,
sandboxConnectRequestTimeoutMs,
- sandboxManagementAuth,
sandboxScoped = false,
teamSlug,
+ userId,
}: DashboardTerminalProps) {
+ const trpcClient = useTRPCClient()
+
const [status, setStatus] = useState('idle')
const [activeSandboxId, setActiveSandboxId] = useState()
const [template, setTemplate] = useState(
@@ -307,12 +309,15 @@ export default function DashboardTerminal({
const terminalSandbox = await openTerminalSandbox({
forceNewSandbox: options.forceNewSandbox,
onStatus: appendOutput,
+ openTerminal: (mutationInput) =>
+ trpcClient.sandbox.openTerminal.mutate(mutationInput),
requestTimeoutMs: requestedSandboxId
? (sandboxConnectRequestTimeoutMs ??
TERMINAL_ATTACH_ATTEMPT_TIMEOUT_MS)
: undefined,
- sandboxManagementAuth,
shouldStoreSession: !sandboxScoped,
+ teamSlug,
+ userId,
sandboxId: requestedSandboxId,
template: nextTemplate,
})
@@ -413,7 +418,9 @@ export default function DashboardTerminal({
focusTerminal,
getSandbox,
runCommand,
- sandboxManagementAuth,
+ trpcClient,
+ teamSlug,
+ userId,
sandboxScoped,
sandboxConnectRequestTimeoutMs,
template,
diff --git a/src/features/dashboard/terminal/sandbox-session.ts b/src/features/dashboard/terminal/sandbox-session.ts
index 37dc43813..8a610a6bd 100644
--- a/src/features/dashboard/terminal/sandbox-session.ts
+++ b/src/features/dashboard/terminal/sandbox-session.ts
@@ -1,18 +1,38 @@
-import Sandbox from 'e2b'
-import type { SandboxManagementAuth } from '@/core/shared/sandbox-management-auth'
-import { TERMINAL_SANDBOX_TIMEOUT_MS } from './constants'
+import type { Sandbox } from 'e2b'
+import { createEnvdSandbox } from '@/core/shared/create-envd-sandbox'
+import type { TRPCRouterOutputs } from '@/trpc/client'
import {
clearStoredTerminalSession,
readStoredTerminalSession,
writeStoredTerminalSession,
} from './storage'
+type TerminalSandboxConnection = TRPCRouterOutputs['sandbox']['openTerminal']
+
+interface OpenTerminalMutationInput {
+ teamSlug: string
+ template: string
+ sandboxId?: string
+ requestTimeoutMs?: number
+}
+
+/**
+ * Performs the `sandbox.openTerminal` tRPC mutation. Injected from the
+ * component (via the vanilla tRPC client) so this orchestration helper stays
+ * usable outside of a React hook.
+ */
+export type OpenTerminalMutation = (
+ input: OpenTerminalMutationInput
+) => Promise
+
interface OpenTerminalSandboxOptions {
forceNewSandbox?: boolean
onStatus: (message: string) => void
+ openTerminal: OpenTerminalMutation
requestTimeoutMs?: number
- sandboxManagementAuth: SandboxManagementAuth
shouldStoreSession?: boolean
+ teamSlug: string
+ userId: string
sandboxId?: string
template: string
}
@@ -20,19 +40,21 @@ interface OpenTerminalSandboxOptions {
export async function openTerminalSandbox({
forceNewSandbox = false,
onStatus,
+ openTerminal,
requestTimeoutMs,
- sandboxManagementAuth,
shouldStoreSession,
+ teamSlug,
+ userId,
sandboxId,
template,
}: OpenTerminalSandboxOptions) {
- const { headers, userId } = sandboxManagementAuth
-
if (sandboxId) {
onStatus(`Connecting to terminal sandbox ${sandboxId}...\r\n`)
- const sandbox = await connectTerminalSandbox(sandboxId, headers, {
- requestTimeoutMs,
- })
+ const sandbox = await acquireTerminalSandbox(
+ openTerminal,
+ { teamSlug, template, sandboxId, requestTimeoutMs },
+ 'Failed to connect to terminal sandbox'
+ )
return {
sandbox,
@@ -51,20 +73,33 @@ export async function openTerminalSandbox({
)
try {
- sandbox = await connectTerminalSandbox(
- storedTerminalSession.sandboxId,
- headers,
- { requestTimeoutMs }
+ sandbox = await acquireTerminalSandbox(
+ openTerminal,
+ {
+ teamSlug,
+ template,
+ sandboxId: storedTerminalSession.sandboxId,
+ requestTimeoutMs,
+ },
+ 'Failed to connect to terminal sandbox'
)
} catch {
clearStoredTerminalSession(userId)
onStatus('Stored terminal sandbox is unavailable.\r\n')
onStatus(`Starting ${template} terminal sandbox...\r\n`)
- sandbox = await createTerminalSandbox({ headers, template, userId })
+ sandbox = await acquireTerminalSandbox(
+ openTerminal,
+ { teamSlug, template },
+ 'Failed to create terminal sandbox'
+ )
}
} else {
onStatus(`Starting ${template} terminal sandbox...\r\n`)
- sandbox = await createTerminalSandbox({ headers, template, userId })
+ sandbox = await acquireTerminalSandbox(
+ openTerminal,
+ { teamSlug, template },
+ 'Failed to create terminal sandbox'
+ )
}
if (shouldStoreSession ?? true) {
@@ -79,48 +114,22 @@ export async function openTerminalSandbox({
}
}
-function connectTerminalSandbox(
- sandboxId: string,
- headers: Record,
- options: { requestTimeoutMs?: number } = {}
-) {
- return Sandbox.connect(sandboxId, {
- apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
- domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
- sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
- timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
- requestTimeoutMs: options.requestTimeoutMs,
- headers: {
- ...headers,
- },
- })
-}
+async function acquireTerminalSandbox(
+ openTerminal: OpenTerminalMutation,
+ input: OpenTerminalMutationInput,
+ fallbackMessage: string
+): Promise {
+ let connection: TerminalSandboxConnection
-function createTerminalSandbox({
- headers,
- template,
- userId,
-}: {
- headers: Record
- template: string
- userId: string
-}) {
- return Sandbox.create(template, {
- apiUrl: process.env.NEXT_PUBLIC_INFRA_API_URL,
+ try {
+ connection = await openTerminal(input)
+ } catch (error) {
+ throw error instanceof Error ? error : new Error(fallbackMessage)
+ }
+
+ return createEnvdSandbox({
+ ...connection,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
- timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS,
- lifecycle: {
- onTimeout: 'pause',
- autoResume: true,
- },
- metadata: {
- source: 'dashboard-terminal',
- template,
- userId,
- },
- headers: {
- ...headers,
- },
})
}
diff --git a/tests/unit/dashboard-terminal.test.ts b/tests/unit/dashboard-terminal.test.ts
index e8ab4b345..3558cb2c2 100644
--- a/tests/unit/dashboard-terminal.test.ts
+++ b/tests/unit/dashboard-terminal.test.ts
@@ -1,9 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import {
- AUTHORIZATION_HEADER,
- BEARER_TOKEN_PREFIX,
- TEAM_ID_HEADER,
-} from '@/configs/api'
import { TERMINAL_SESSION_STORAGE_PREFIX } from '@/features/dashboard/terminal/constants'
import { openTerminalSandbox } from '@/features/dashboard/terminal/sandbox-session'
import {
@@ -17,18 +12,19 @@ import {
} from '@/features/dashboard/terminal/template'
import { calculateTerminalSize } from '@/features/dashboard/terminal/terminal-size'
-const { mockCreateSandbox, mockConnectSandbox } = vi.hoisted(() => ({
- mockCreateSandbox: vi.fn(),
- mockConnectSandbox: vi.fn(),
+const { mockCreateEnvdSandbox } = vi.hoisted(() => ({
+ mockCreateEnvdSandbox: vi.fn(),
}))
-vi.mock('e2b', () => ({
- default: {
- connect: mockConnectSandbox,
- create: mockCreateSandbox,
- },
+vi.mock('@/core/shared/create-envd-sandbox', () => ({
+ createEnvdSandbox: mockCreateEnvdSandbox,
}))
+// The `sandbox.openTerminal` tRPC mutation is injected into
+// openTerminalSandbox, so the test passes this mock directly instead of
+// mocking a module.
+const mockOpenTerminal = vi.fn()
+
function installLocalStorage() {
const values = new Map()
@@ -52,19 +48,23 @@ function installLocalStorage() {
}
describe('dashboard terminal helpers', () => {
- const sandboxManagementAuth = {
- headers: {
- [AUTHORIZATION_HEADER]: `${BEARER_TOKEN_PREFIX}auth-provider-token`,
- [TEAM_ID_HEADER]: 'team-123',
- },
- userId: 'user-123',
- }
-
beforeEach(() => {
vi.clearAllMocks()
installLocalStorage()
- mockCreateSandbox.mockResolvedValue({ sandboxId: 'created-sandbox' })
- mockConnectSandbox.mockResolvedValue({ sandboxId: 'connected-sandbox' })
+ // The tRPC mutation returns sandbox-scoped envd credentials; for an
+ // explicit/stored sandbox id it echoes that id back, otherwise it reports
+ // the freshly created sandbox id.
+ mockOpenTerminal.mockImplementation(
+ async (input: { sandboxId?: string }) => ({
+ sandboxId: input.sandboxId ?? 'created-sandbox',
+ sandboxDomain: 'sandbox.example.com',
+ envdVersion: '0.2.0',
+ envdAccessToken: 'envd-token',
+ })
+ )
+ mockCreateEnvdSandbox.mockImplementation(
+ (params: { sandboxId: string }) => ({ sandboxId: params.sandboxId })
+ )
})
describe('normalizeTerminalTemplate', () => {
@@ -267,50 +267,72 @@ describe('dashboard terminal helpers', () => {
await openTerminalSandbox({
onStatus: (message) => statuses.push(message),
- sandboxManagementAuth,
+ openTerminal: mockOpenTerminal,
+ teamSlug: 'team-slug',
+ userId: 'user-123',
sandboxId: 'sandbox-from-url',
template: 'base',
})
- expect(mockConnectSandbox).toHaveBeenCalledWith('sandbox-from-url', {
- domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
- timeoutMs: 30 * 60 * 1000,
+ expect(mockOpenTerminal).toHaveBeenCalledWith({
+ teamSlug: 'team-slug',
+ template: 'base',
+ sandboxId: 'sandbox-from-url',
requestTimeoutMs: undefined,
- headers: {
- [AUTHORIZATION_HEADER]: `${BEARER_TOKEN_PREFIX}auth-provider-token`,
- [TEAM_ID_HEADER]: 'team-123',
- },
})
- expect(mockCreateSandbox).not.toHaveBeenCalled()
+ expect(mockCreateEnvdSandbox).toHaveBeenCalledWith({
+ sandboxId: 'sandbox-from-url',
+ sandboxDomain: 'sandbox.example.com',
+ envdVersion: '0.2.0',
+ envdAccessToken: 'envd-token',
+ domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
+ sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
+ })
expect(readStoredTerminalSession('user-123')).toBeNull()
expect(statuses).toEqual([
'Connecting to terminal sandbox sandbox-from-url...\r\n',
])
})
- it('creates and stores a terminal sandbox when no reusable session exists', async () => {
+ it('connects to a tokenless (secure: false) sandbox without an envd access token', async () => {
+ mockOpenTerminal.mockResolvedValueOnce({
+ sandboxId: 'insecure-sandbox',
+ sandboxDomain: 'sandbox.example.com',
+ envdVersion: '0.2.0',
+ envdAccessToken: undefined,
+ })
+
await openTerminalSandbox({
onStatus: vi.fn(),
- sandboxManagementAuth,
+ openTerminal: mockOpenTerminal,
+ teamSlug: 'team-slug',
+ userId: 'user-123',
+ sandboxId: 'insecure-sandbox',
template: 'base',
})
- expect(mockCreateSandbox).toHaveBeenCalledWith('base', {
+ expect(mockCreateEnvdSandbox).toHaveBeenCalledWith({
+ sandboxId: 'insecure-sandbox',
+ sandboxDomain: 'sandbox.example.com',
+ envdVersion: '0.2.0',
+ envdAccessToken: undefined,
domain: process.env.NEXT_PUBLIC_E2B_DOMAIN,
- timeoutMs: 30 * 60 * 1000,
- lifecycle: {
- onTimeout: 'pause',
- autoResume: true,
- },
- metadata: {
- source: 'dashboard-terminal',
- template: 'base',
- userId: 'user-123',
- },
- headers: {
- [AUTHORIZATION_HEADER]: `${BEARER_TOKEN_PREFIX}auth-provider-token`,
- [TEAM_ID_HEADER]: 'team-123',
- },
+ sandboxUrl: process.env.NEXT_PUBLIC_E2B_SANDBOX_URL,
+ })
+ })
+
+ it('creates and stores a terminal sandbox when no reusable session exists', async () => {
+ await openTerminalSandbox({
+ onStatus: vi.fn(),
+ openTerminal: mockOpenTerminal,
+ teamSlug: 'team-slug',
+ userId: 'user-123',
+ template: 'base',
+ })
+
+ expect(mockOpenTerminal).toHaveBeenCalledWith({
+ teamSlug: 'team-slug',
+ template: 'base',
})
expect(readStoredTerminalSession('user-123')).toEqual({
sandboxId: 'created-sandbox',
@@ -326,15 +348,53 @@ describe('dashboard terminal helpers', () => {
await openTerminalSandbox({
onStatus: vi.fn(),
- sandboxManagementAuth,
+ openTerminal: mockOpenTerminal,
+ teamSlug: 'team-slug',
+ userId: 'user-123',
template: 'base',
})
- expect(mockConnectSandbox).toHaveBeenCalledWith(
- 'stored-sandbox',
- expect.anything()
- )
- expect(mockCreateSandbox).not.toHaveBeenCalled()
+ expect(mockOpenTerminal).toHaveBeenCalledWith({
+ teamSlug: 'team-slug',
+ template: 'base',
+ sandboxId: 'stored-sandbox',
+ requestTimeoutMs: undefined,
+ })
+ })
+
+ it('falls back to creating a new sandbox when reconnecting fails', async () => {
+ writeStoredTerminalSession('user-123', {
+ sandboxId: 'stored-sandbox',
+ template: 'base',
+ })
+
+ mockOpenTerminal.mockImplementationOnce(async () => {
+ throw new Error('Failed to connect to terminal sandbox')
+ })
+
+ await openTerminalSandbox({
+ onStatus: vi.fn(),
+ openTerminal: mockOpenTerminal,
+ teamSlug: 'team-slug',
+ userId: 'user-123',
+ template: 'base',
+ })
+
+ // First call attempts the stored sandbox, second call creates a new one.
+ expect(mockOpenTerminal).toHaveBeenNthCalledWith(1, {
+ teamSlug: 'team-slug',
+ template: 'base',
+ sandboxId: 'stored-sandbox',
+ requestTimeoutMs: undefined,
+ })
+ expect(mockOpenTerminal).toHaveBeenNthCalledWith(2, {
+ teamSlug: 'team-slug',
+ template: 'base',
+ })
+ expect(readStoredTerminalSession('user-123')).toEqual({
+ sandboxId: 'created-sandbox',
+ template: 'base',
+ })
})
})
})
diff --git a/tests/unit/sandbox-side-effects.test.ts b/tests/unit/sandbox-side-effects.test.ts
new file mode 100644
index 000000000..c455d4c61
--- /dev/null
+++ b/tests/unit/sandbox-side-effects.test.ts
@@ -0,0 +1,148 @@
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { createTRPCContext } from '@/core/server/trpc/init'
+import { SANDBOX_RESUME_TIMEOUT_MS } from '@/features/dashboard/sandbox/inspect/constants'
+import { TERMINAL_SANDBOX_TIMEOUT_MS } from '@/features/dashboard/terminal/constants'
+
+/**
+ * Guards the lifecycle invariant for the sandbox debug/inspect views:
+ *
+ * - Connecting to a sandbox from the inspect/terminal CLIENT must never touch
+ * the control plane (`Sandbox.connect`/`Sandbox.create`), because those
+ * resume paused sandboxes and can extend a running sandbox's TTL.
+ * - The only control-plane calls live in the tRPC router, and they happen on
+ * EXPLICIT user actions (open terminal / resume) with a deliberate,
+ * bounded `timeoutMs`.
+ */
+
+const sdkMock = vi.hoisted(() => ({
+ connect: vi.fn(),
+ create: vi.fn(),
+ getFullInfo: vi.fn(),
+}))
+
+vi.mock('e2b', () => ({
+ Sandbox: {
+ connect: sdkMock.connect,
+ create: sdkMock.create,
+ getFullInfo: sdkMock.getFullInfo,
+ },
+ TimeoutError: class TimeoutError extends Error {},
+}))
+
+const authMock = vi.hoisted(() => ({ getAuthContext: vi.fn() }))
+vi.mock('@/core/server/auth', () => ({
+ getAuthContext: authMock.getAuthContext,
+}))
+
+const teamMock = vi.hoisted(() => ({ getTeamIdFromSlug: vi.fn() }))
+vi.mock('@/core/server/functions/team/get-team-id-from-slug', () => ({
+ getTeamIdFromSlug: teamMock.getTeamIdFromSlug,
+}))
+
+const { createCallerFactory } = await import('@/core/server/trpc/init')
+const { sandboxRouter } = await import('@/core/server/api/routers/sandbox')
+
+const createCaller = createCallerFactory(sandboxRouter)
+
+async function caller() {
+ const ctx = await createTRPCContext({ headers: new Headers() })
+ return createCaller(ctx)
+}
+
+describe('sandbox lifecycle side effects', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ authMock.getAuthContext.mockResolvedValue({
+ user: { id: 'user-1' },
+ accessToken: 'access-token',
+ })
+ teamMock.getTeamIdFromSlug.mockResolvedValue({ ok: true, data: 'team-1' })
+ sdkMock.connect.mockResolvedValue({ sandboxId: 'sbx_existing' })
+ sdkMock.create.mockResolvedValue({ sandboxId: 'sbx_new' })
+ sdkMock.getFullInfo.mockResolvedValue({
+ sandboxDomain: 'sandbox.example.com',
+ envdVersion: '0.2.0',
+ envdAccessToken: 'envd-token',
+ })
+ })
+
+ describe('sandbox.resume (explicit user action)', () => {
+ it('resumes via the control plane with the bounded resume TTL only', async () => {
+ const c = await caller()
+ const result = await c.resume({
+ teamSlug: 'team-slug',
+ sandboxId: 'sbxexisting',
+ })
+
+ // Resume is the only inspect-side control-plane connect, and it sets an
+ // explicit, bounded TTL — never the SDK default.
+ expect(sdkMock.connect).toHaveBeenCalledTimes(1)
+ expect(sdkMock.connect).toHaveBeenCalledWith(
+ 'sbxexisting',
+ expect.objectContaining({ timeoutMs: SANDBOX_RESUME_TIMEOUT_MS })
+ )
+ expect(sdkMock.create).not.toHaveBeenCalled()
+ expect(result.envdAccessToken).toBe('envd-token')
+ })
+ })
+
+ describe('sandbox.openTerminal (explicit user action)', () => {
+ it('connects to an existing sandbox with the explicit terminal TTL', async () => {
+ const c = await caller()
+ await c.openTerminal({
+ teamSlug: 'team-slug',
+ template: 'base',
+ sandboxId: 'sbxexisting',
+ })
+
+ expect(sdkMock.connect).toHaveBeenCalledTimes(1)
+ expect(sdkMock.connect).toHaveBeenCalledWith(
+ 'sbxexisting',
+ expect.objectContaining({ timeoutMs: TERMINAL_SANDBOX_TIMEOUT_MS })
+ )
+ expect(sdkMock.create).not.toHaveBeenCalled()
+ })
+
+ it('creates a new sandbox (never connect) when no sandbox id is given', async () => {
+ const c = await caller()
+ await c.openTerminal({ teamSlug: 'team-slug', template: 'base' })
+
+ expect(sdkMock.create).toHaveBeenCalledTimes(1)
+ expect(sdkMock.connect).not.toHaveBeenCalled()
+ })
+ })
+})
+
+describe('sandbox inspect/terminal client never calls the control plane', () => {
+ const read = (relativePath: string) =>
+ readFileSync(join(process.cwd(), relativePath), 'utf8')
+
+ // Client modules that connect to sandboxes for the debug/inspect views.
+ const clientModules = [
+ 'src/features/dashboard/sandbox/inspect/context.tsx',
+ 'src/features/dashboard/terminal/sandbox-session.ts',
+ 'src/features/dashboard/terminal/dashboard-terminal.tsx',
+ 'src/features/dashboard/sandbox/terminal/view.tsx',
+ ]
+
+ it.each(
+ clientModules
+ )('%s does not call Sandbox.connect / Sandbox.create', (modulePath) => {
+ const source = read(modulePath)
+ expect(source).not.toMatch(/Sandbox\.connect\s*\(/)
+ expect(source).not.toMatch(/Sandbox\.create\s*\(/)
+ })
+
+ it('the inspect context builds an envd-only client via createEnvdSandbox', () => {
+ const source = read('src/features/dashboard/sandbox/inspect/context.tsx')
+ expect(source).toMatch(/createEnvdSandbox\(/)
+ })
+
+ it('createEnvdSandbox constructs envd-only and never calls the control plane', () => {
+ const source = read('src/core/shared/create-envd-sandbox.ts')
+ expect(source).not.toMatch(/Sandbox\.connect\s*\(/)
+ expect(source).not.toMatch(/Sandbox\.create\s*\(/)
+ })
+})