chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { assert, test, describe } from 'vitest'
|
||||
import { handleEnvdApiError, handleEnvdApiFetchError } from '../../src/envd/api'
|
||||
import {
|
||||
AuthenticationError,
|
||||
InvalidArgumentError,
|
||||
NotEnoughSpaceError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
SandboxError,
|
||||
TimeoutError,
|
||||
} from '../../src/errors'
|
||||
|
||||
function createMockResponse(
|
||||
status: number,
|
||||
error?: { message?: string } | string
|
||||
): {
|
||||
error?: { message?: string } | string
|
||||
response: Response
|
||||
} {
|
||||
return {
|
||||
error,
|
||||
response: {
|
||||
status,
|
||||
ok: status >= 200 && status < 300,
|
||||
statusText: '',
|
||||
// openapi-fetch consumes the body whenever it produces an error value
|
||||
bodyUsed: error !== undefined,
|
||||
text: async () => (typeof error === 'string' ? error : ''),
|
||||
} as unknown as Response,
|
||||
}
|
||||
}
|
||||
|
||||
describe('handleEnvdApiError', () => {
|
||||
test('returns undefined for a successful response', async () => {
|
||||
const err = await handleEnvdApiError(createMockResponse(200))
|
||||
assert.isUndefined(err)
|
||||
})
|
||||
|
||||
test('returns an error for non-2xx response without content', async () => {
|
||||
// openapi-fetch leaves `error` undefined for responses with
|
||||
// Content-Length: 0
|
||||
const res = createMockResponse(500)
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '500')
|
||||
})
|
||||
|
||||
test('returns an error for non-2xx response with empty string error', async () => {
|
||||
const res = createMockResponse(500, '')
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '500')
|
||||
})
|
||||
|
||||
test('returns a mapped error for non-2xx response without content', async () => {
|
||||
const res = createMockResponse(404)
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, NotFoundError)
|
||||
})
|
||||
|
||||
test('returns InvalidArgumentError for 400', async () => {
|
||||
const res = createMockResponse(400, { message: 'Bad request' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, InvalidArgumentError)
|
||||
})
|
||||
|
||||
test('returns AuthenticationError for 401', async () => {
|
||||
const res = createMockResponse(401, { message: 'Invalid token' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
})
|
||||
|
||||
test('returns NotFoundError for 404', async () => {
|
||||
const res = createMockResponse(404, { message: 'Not found' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, NotFoundError)
|
||||
})
|
||||
|
||||
test('returns RateLimitError for 429', async () => {
|
||||
const res = createMockResponse(429, { message: 'Too many requests' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, RateLimitError)
|
||||
assert.include(err?.message, 'rate limited')
|
||||
})
|
||||
|
||||
test('returns TimeoutError for 502', async () => {
|
||||
const res = createMockResponse(502, { message: 'Bad gateway' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
})
|
||||
|
||||
test('returns NotEnoughSpaceError for 507', async () => {
|
||||
const res = createMockResponse(507, { message: 'No space left' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, NotEnoughSpaceError)
|
||||
})
|
||||
|
||||
test('falls back to SandboxError for unmapped status', async () => {
|
||||
const res = createMockResponse(500, { message: 'Internal error' })
|
||||
const err = await handleEnvdApiError(res)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err?.message, '500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleEnvdApiFetchError', () => {
|
||||
test('returns the original error for terminated fetch without a health check', async () => {
|
||||
const original = new TypeError('terminated')
|
||||
const err = await handleEnvdApiFetchError(original)
|
||||
assert.strictEqual(err, original)
|
||||
})
|
||||
|
||||
test('returns a TimeoutError when the health check says the sandbox is not running', async () => {
|
||||
const err = await handleEnvdApiFetchError(
|
||||
new TypeError('terminated'),
|
||||
async () => false
|
||||
)
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
assert.include(err.message, 'sandbox was killed or reached its end of life')
|
||||
})
|
||||
|
||||
// Each JS runtime surfaces a dropped connection with different wording, and not
|
||||
// always as a TypeError (Bun raises a plain Error), so match by message
|
||||
const runtimeTerminatedErrors = {
|
||||
Node: new TypeError('terminated'),
|
||||
Bun: new Error('The socket connection was closed unexpectedly'),
|
||||
Deno: new TypeError('error reading a body from connection'),
|
||||
}
|
||||
|
||||
for (const [runtime, error] of Object.entries(runtimeTerminatedErrors)) {
|
||||
test(`treats the ${runtime} dropped-connection error as terminated`, async () => {
|
||||
const err = await handleEnvdApiFetchError(error, async () => false)
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
assert.include(
|
||||
err.message,
|
||||
'sandbox was killed or reached its end of life'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
test('returns the original error when the health check says the sandbox is running', async () => {
|
||||
const original = new TypeError('terminated')
|
||||
const err = await handleEnvdApiFetchError(original, async () => true)
|
||||
assert.strictEqual(err, original)
|
||||
})
|
||||
|
||||
test('returns the original error for other fetch failures', async () => {
|
||||
const original = new TypeError('fetch failed')
|
||||
const err = await handleEnvdApiFetchError(original)
|
||||
assert.strictEqual(err, original)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,138 @@
|
||||
import { assert, test, describe } from 'vitest'
|
||||
import { Code, ConnectError } from '@connectrpc/connect'
|
||||
import {
|
||||
handleRpcError,
|
||||
handleRpcErrorWithHealthCheck,
|
||||
} from '../../src/envd/rpc'
|
||||
import {
|
||||
AuthenticationError,
|
||||
InvalidArgumentError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
SandboxError,
|
||||
TimeoutError,
|
||||
} from '../../src/errors'
|
||||
|
||||
describe('handleRpcError', () => {
|
||||
test('returns InvalidArgumentError for InvalidArgument', () => {
|
||||
const err = handleRpcError(new ConnectError('bad', Code.InvalidArgument))
|
||||
assert.instanceOf(err, InvalidArgumentError)
|
||||
})
|
||||
|
||||
test('returns AuthenticationError for Unauthenticated', () => {
|
||||
const err = handleRpcError(new ConnectError('nope', Code.Unauthenticated))
|
||||
assert.instanceOf(err, AuthenticationError)
|
||||
})
|
||||
|
||||
test('returns NotFoundError for NotFound', () => {
|
||||
const err = handleRpcError(new ConnectError('missing', Code.NotFound))
|
||||
assert.instanceOf(err, NotFoundError)
|
||||
})
|
||||
|
||||
test('returns RateLimitError for ResourceExhausted', () => {
|
||||
const err = handleRpcError(
|
||||
new ConnectError('too many', Code.ResourceExhausted)
|
||||
)
|
||||
assert.instanceOf(err, RateLimitError)
|
||||
assert.include(err.message, 'Rate limit')
|
||||
})
|
||||
|
||||
test('returns TimeoutError for Unavailable', () => {
|
||||
const err = handleRpcError(new ConnectError('gone', Code.Unavailable))
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
})
|
||||
|
||||
test('falls back to SandboxError for unmapped code', () => {
|
||||
const err = handleRpcError(new ConnectError('boom', Code.Internal))
|
||||
assert.instanceOf(err, SandboxError)
|
||||
})
|
||||
|
||||
test('falls back to generic SandboxError for Unknown "terminated"', () => {
|
||||
const err = handleRpcError(new ConnectError('terminated', Code.Unknown))
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.include(err.message, 'terminated')
|
||||
assert.notInclude(err.message, 'killed')
|
||||
})
|
||||
|
||||
test('returns the original error when not a ConnectError', () => {
|
||||
const original = new Error('not connect')
|
||||
const err = handleRpcError(original)
|
||||
assert.strictEqual(err, original)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleRpcErrorWithHealthCheck', () => {
|
||||
const terminated = () => new ConnectError('terminated', Code.Unknown)
|
||||
|
||||
// Each JS runtime surfaces a dropped connection with different wording
|
||||
const runtimeTerminatedMessages = {
|
||||
Node: 'terminated',
|
||||
Bun: 'The socket connection was closed unexpectedly',
|
||||
Deno: 'error reading a body from connection',
|
||||
}
|
||||
|
||||
test('returns a TimeoutError when the health check says the sandbox is not running', async () => {
|
||||
const err = await handleRpcErrorWithHealthCheck(
|
||||
terminated(),
|
||||
async () => false
|
||||
)
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
assert.include(err.message, 'sandbox was killed or reached its end of life')
|
||||
})
|
||||
|
||||
for (const [runtime, message] of Object.entries(runtimeTerminatedMessages)) {
|
||||
test(`treats the ${runtime} dropped-connection message as terminated`, async () => {
|
||||
const err = await handleRpcErrorWithHealthCheck(
|
||||
new ConnectError(message, Code.Unknown),
|
||||
async () => false
|
||||
)
|
||||
assert.instanceOf(err, TimeoutError)
|
||||
assert.include(
|
||||
err.message,
|
||||
'sandbox was killed or reached its end of life'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
test('falls back to the generic mapping when the health check says the sandbox is running', async () => {
|
||||
const err = await handleRpcErrorWithHealthCheck(
|
||||
terminated(),
|
||||
async () => true
|
||||
)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.notInstanceOf(err, TimeoutError)
|
||||
assert.notInclude(err.message, 'killed')
|
||||
})
|
||||
|
||||
test('falls back to the generic mapping when the sandbox state is unknown', async () => {
|
||||
const err = await handleRpcErrorWithHealthCheck(
|
||||
terminated(),
|
||||
async () => undefined
|
||||
)
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.notInstanceOf(err, TimeoutError)
|
||||
assert.notInclude(err.message, 'killed')
|
||||
})
|
||||
|
||||
test('falls back to the generic mapping when the health check itself fails', async () => {
|
||||
const err = await handleRpcErrorWithHealthCheck(terminated(), async () => {
|
||||
throw new Error('health check failed')
|
||||
})
|
||||
assert.instanceOf(err, SandboxError)
|
||||
assert.notInstanceOf(err, TimeoutError)
|
||||
assert.notInclude(err.message, 'killed')
|
||||
})
|
||||
|
||||
test('does not run the health check for other errors', async () => {
|
||||
let called = false
|
||||
const err = await handleRpcErrorWithHealthCheck(
|
||||
new ConnectError('missing', Code.NotFound),
|
||||
async () => {
|
||||
called = true
|
||||
return false
|
||||
}
|
||||
)
|
||||
assert.instanceOf(err, NotFoundError)
|
||||
assert.isFalse(called)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,276 @@
|
||||
import { afterEach, expect, test, vi } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.resetModules()
|
||||
vi.doUnmock('undici')
|
||||
vi.doUnmock('../../src/utils')
|
||||
delete process.env.E2B_ENVD_RPC_CONNECTIONS
|
||||
delete process.env.E2B_ENVD_INFLIGHT_REQUESTS
|
||||
delete process.env.E2B_ENVD_RPC_INFLIGHT_REQUESTS
|
||||
})
|
||||
|
||||
test('uses undici with HTTP/2 enabled in Node', async () => {
|
||||
const agents: Array<{ allowH2?: boolean; connections?: number }> = []
|
||||
const requests: Array<{ init?: RequestInit & { dispatcher?: unknown } }> = []
|
||||
|
||||
class Agent {
|
||||
constructor(options: { allowH2?: boolean; connections?: number }) {
|
||||
agents.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
const undiciFetch = vi.fn((input, init) => {
|
||||
requests.push({ init })
|
||||
return Promise.resolve(new Response('ok'))
|
||||
})
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
connectionLimit: 1,
|
||||
loadUndici: () => Promise.resolve({ Agent, fetch: undiciFetch }),
|
||||
})
|
||||
const res = await fetcher('https://example.com/status')
|
||||
|
||||
expect(await res.text()).toBe('ok')
|
||||
expect(agents).toEqual([{ allowH2: true, connections: 1 }])
|
||||
expect(requests[0].init?.dispatcher).toBeInstanceOf(Agent)
|
||||
})
|
||||
|
||||
test('uses a ProxyAgent dispatcher when a proxy is configured', async () => {
|
||||
const proxyAgents: Array<{
|
||||
uri?: string
|
||||
allowH2?: boolean
|
||||
connections?: number
|
||||
}> = []
|
||||
const agents: Array<unknown> = []
|
||||
const requests: Array<{ init?: RequestInit & { dispatcher?: unknown } }> = []
|
||||
|
||||
class Agent {
|
||||
constructor() {
|
||||
agents.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
class ProxyAgent {
|
||||
constructor(options: {
|
||||
uri?: string
|
||||
allowH2?: boolean
|
||||
connections?: number
|
||||
}) {
|
||||
proxyAgents.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
const undiciFetch = vi.fn((input, init) => {
|
||||
requests.push({ init })
|
||||
return Promise.resolve(new Response('ok'))
|
||||
})
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
connectionLimit: 1,
|
||||
proxy: 'http://127.0.0.1:8080',
|
||||
loadUndici: () =>
|
||||
Promise.resolve({ Agent, ProxyAgent, fetch: undiciFetch }),
|
||||
})
|
||||
await fetcher('https://example.com/status')
|
||||
|
||||
expect(agents).toHaveLength(0)
|
||||
expect(proxyAgents).toEqual([
|
||||
{ uri: 'http://127.0.0.1:8080', allowH2: true, connections: 1 },
|
||||
])
|
||||
expect(requests[0].init?.dispatcher).toBeInstanceOf(ProxyAgent)
|
||||
})
|
||||
|
||||
test('caches envd fetchers per proxy', async () => {
|
||||
const { createEnvdFetch, createEnvdRpcFetch } = await import(
|
||||
'../../src/envd/http2'
|
||||
)
|
||||
|
||||
const noProxy = createEnvdFetch()
|
||||
const proxyA = createEnvdFetch('http://127.0.0.1:8080')
|
||||
|
||||
expect(createEnvdFetch()).toBe(noProxy)
|
||||
expect(createEnvdFetch('http://127.0.0.1:8080')).toBe(proxyA)
|
||||
expect(proxyA).not.toBe(noProxy)
|
||||
|
||||
const rpcNoProxy = createEnvdRpcFetch()
|
||||
const rpcProxyA = createEnvdRpcFetch('http://127.0.0.1:8080')
|
||||
|
||||
expect(createEnvdRpcFetch()).toBe(rpcNoProxy)
|
||||
expect(createEnvdRpcFetch('http://127.0.0.1:8080')).toBe(rpcProxyA)
|
||||
expect(rpcProxyA).not.toBe(rpcNoProxy)
|
||||
})
|
||||
|
||||
test('passes Request objects to undici as URL plus init', async () => {
|
||||
const requests: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []
|
||||
|
||||
class Agent {}
|
||||
|
||||
const undiciFetch = vi.fn((input, init) => {
|
||||
requests.push({ input, init })
|
||||
return Promise.resolve(new Response('ok'))
|
||||
})
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
connectionLimit: 1,
|
||||
loadUndici: () => Promise.resolve({ Agent, fetch: undiciFetch }),
|
||||
})
|
||||
const body = JSON.stringify({ ok: true })
|
||||
await fetcher(
|
||||
new Request('https://example.com/rpc', {
|
||||
body,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
method: 'POST',
|
||||
})
|
||||
)
|
||||
|
||||
expect(requests[0].input).toBe('https://example.com/rpc')
|
||||
expect(requests[0].init?.method).toBe('POST')
|
||||
expect(requests[0].init?.headers).toBeInstanceOf(Headers)
|
||||
expect(requests[0].init?.body).toBeInstanceOf(ReadableStream)
|
||||
})
|
||||
|
||||
test('can create a bounded dispatcher for RPC streams', async () => {
|
||||
const agents: Array<{ allowH2?: boolean; connections?: number }> = []
|
||||
|
||||
class Agent {
|
||||
constructor(options: { allowH2?: boolean; connections?: number }) {
|
||||
agents.push(options)
|
||||
}
|
||||
}
|
||||
|
||||
const undiciFetch = vi.fn(() => Promise.resolve(new Response('ok')))
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
connectionLimit: 100,
|
||||
loadUndici: () => Promise.resolve({ Agent, fetch: undiciFetch }),
|
||||
})
|
||||
await fetcher('https://example.com/rpc')
|
||||
|
||||
expect(agents).toEqual([{ allowH2: true, connections: 100 }])
|
||||
})
|
||||
|
||||
test('reads RPC stream dispatcher connection limit from env', async () => {
|
||||
process.env.E2B_ENVD_RPC_CONNECTIONS = '200'
|
||||
|
||||
const { getEnvdRpcConnectionLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(getEnvdRpcConnectionLimit()).toBe(200)
|
||||
})
|
||||
|
||||
test('getEnvdRpcConnectionLimit throws on malformed env value', async () => {
|
||||
process.env.E2B_ENVD_RPC_CONNECTIONS = 'bogus'
|
||||
|
||||
const { getEnvdRpcConnectionLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(() => getEnvdRpcConnectionLimit()).toThrow(/E2B_ENVD_RPC_CONNECTIONS/)
|
||||
})
|
||||
|
||||
test('getEnvdInflightLimit throws on malformed env value', async () => {
|
||||
process.env.E2B_ENVD_INFLIGHT_REQUESTS = 'bogus'
|
||||
|
||||
const { getEnvdInflightLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(() => getEnvdInflightLimit()).toThrow(/E2B_ENVD_INFLIGHT_REQUESTS/)
|
||||
})
|
||||
|
||||
test('getEnvdRpcInflightLimit throws on malformed env value', async () => {
|
||||
process.env.E2B_ENVD_RPC_INFLIGHT_REQUESTS = 'bogus'
|
||||
|
||||
const { getEnvdRpcInflightLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(() => getEnvdRpcInflightLimit()).toThrow(
|
||||
/E2B_ENVD_RPC_INFLIGHT_REQUESTS/
|
||||
)
|
||||
})
|
||||
|
||||
test('inflight limit env vars return 0 when explicitly disabled', async () => {
|
||||
process.env.E2B_ENVD_INFLIGHT_REQUESTS = '0'
|
||||
process.env.E2B_ENVD_RPC_INFLIGHT_REQUESTS = '0'
|
||||
|
||||
const { getEnvdInflightLimit, getEnvdRpcInflightLimit } = await import(
|
||||
'../../src/envd/http2'
|
||||
)
|
||||
|
||||
expect(getEnvdInflightLimit()).toBe(0)
|
||||
expect(getEnvdRpcInflightLimit()).toBe(0)
|
||||
})
|
||||
|
||||
test('getEnvdInflightLimit throws on negative env value', async () => {
|
||||
process.env.E2B_ENVD_INFLIGHT_REQUESTS = '-1'
|
||||
|
||||
const { getEnvdInflightLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(() => getEnvdInflightLimit()).toThrow(/E2B_ENVD_INFLIGHT_REQUESTS=-1/)
|
||||
})
|
||||
|
||||
test('getEnvdRpcInflightLimit throws on negative env value', async () => {
|
||||
process.env.E2B_ENVD_RPC_INFLIGHT_REQUESTS = '-5'
|
||||
|
||||
const { getEnvdRpcInflightLimit } = await import('../../src/envd/http2')
|
||||
|
||||
expect(() => getEnvdRpcInflightLimit()).toThrow(
|
||||
/E2B_ENVD_RPC_INFLIGHT_REQUESTS=-5/
|
||||
)
|
||||
})
|
||||
|
||||
test('defers loading undici until the first Node request', async () => {
|
||||
const Agent = vi.fn()
|
||||
const undiciFetch = vi.fn(() => Promise.resolve(new Response('ok')))
|
||||
const loadUndici = vi.fn(() => Promise.resolve({ Agent, fetch: undiciFetch }))
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
connectionLimit: 1,
|
||||
loadUndici,
|
||||
})
|
||||
|
||||
expect(loadUndici).not.toHaveBeenCalled()
|
||||
expect(Agent).not.toHaveBeenCalled()
|
||||
expect(undiciFetch).not.toHaveBeenCalled()
|
||||
|
||||
await fetcher('https://example.com/status')
|
||||
|
||||
expect(loadUndici).toHaveBeenCalledOnce()
|
||||
expect(Agent).toHaveBeenCalledOnce()
|
||||
expect(undiciFetch).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
test('falls back to global fetch when undici cannot be loaded', async () => {
|
||||
const fallbackFetch = vi.fn(() =>
|
||||
Promise.resolve(new Response('fallback ok'))
|
||||
) as unknown as typeof fetch
|
||||
vi.stubGlobal('fetch', fallbackFetch)
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
const fetcher = createEnvdFetchForRuntime('node', {
|
||||
loadUndici: () => Promise.resolve(undefined),
|
||||
})
|
||||
const res = await fetcher('https://example.com/status')
|
||||
|
||||
expect(await res.text()).toBe('fallback ok')
|
||||
expect(fallbackFetch).toHaveBeenCalledWith(
|
||||
'https://example.com/status',
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
test('uses global fetch outside Node', async () => {
|
||||
const fallbackFetch = vi.fn() as unknown as typeof fetch
|
||||
vi.stubGlobal('fetch', fallbackFetch)
|
||||
|
||||
const { createEnvdFetchForRuntime } = await import('../../src/envd/http2')
|
||||
|
||||
expect(createEnvdFetchForRuntime('browser')).toBe(fallbackFetch)
|
||||
expect(createEnvdFetchForRuntime('vercel-edge')).toBe(fallbackFetch)
|
||||
})
|
||||
Reference in New Issue
Block a user