From f57c67cb7766b1db597619b84bbfb4f1dcb0f465 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:38:31 +0200 Subject: [PATCH 1/9] test(js-sdk): run the full unit test suite in a browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes the browser leg from a single React smoke test to the full unit + connectionConfig suite running inside a real headless Chromium via @vitest/browser + Playwright (`pnpm test:browser`) — the same coverage test:bun / test:deno / test:cf get. 69 files / 343 tests green. The React smoke test and its devDependencies are gone (react, react-dom, @types/react{,-dom}, @testing-library/react, @vitejs/plugin-react, vitest-browser-react). The one thing it uniquely covered — the SDK driven with no `process` global at all, as in a real browser bundle — is kept as tests/runtimes/browser/noProcessGlobal.test.ts. Chromium installation moves from the default `pretest` hook to `pretest:browser`, so plain `pnpm test` no longer downloads a browser. Test bugs the leg surfaced: - commands/kill and pty/kill asserted `rejects.toThrowError(ProcessExitError)`, but `ProcessExitError` is not exported from src. Vite's SSR transform resolved the named import to `undefined`, so both assertions were vacuous; real ESM in the browser makes it a hard SyntaxError. Now CommandExitError. - files/read.test.ts had three byte-identical duplicated tests, each provisioning a second sandbox on every runtime. - api/http2 and envd/http2 carried dead `vi.doUnmock('undici')` / `vi.doUnmock('../../src/utils')` calls from an old refactor. The bare 'undici' one made Vite pre-bundle a Node HTTP client mid-run and reload the page under the running suite — a cold-cache-only flake that broke whichever file was being collected at the time. Node-only test APIs are replaced with cross-runtime equivalents: node:crypto randomUUID/createHash to WebCrypto, Buffer to TextEncoder/TextDecoder, path.basename inlined. Limitations a browser physically can't work around are gated by documented capability flags in tests/setup.ts, so they report as skipped instead of disappearing into the config's exclude list. Two are real user-facing bugs, filed as SDK-293 (x-next-token is not in Access-Control-Expose-Headers, so Sandbox.list pagination silently truncates) and SDK-294 (the stopped-sandbox 502 has no CORS headers, so isRunning() throws instead of returning false). Closes SDK-292 Co-Authored-By: Claude --- .github/workflows/js_sdk_tests.yml | 34 +- packages/js-sdk/.gitignore | 2 + packages/js-sdk/package.json | 9 +- packages/js-sdk/tests/api/http2.test.ts | 2 - packages/js-sdk/tests/api/list.test.ts | 19 +- packages/js-sdk/tests/api/snapshot.test.ts | 46 +- .../js-sdk/tests/connectionConfig.test.ts | 36 +- packages/js-sdk/tests/envd/http2.test.ts | 2 - packages/js-sdk/tests/runtime.test.ts | 9 +- .../tests/runtimes/browser/importMeta.d.ts | 7 + .../runtimes/browser/noProcessGlobal.test.ts | 45 ++ .../runtimes/browser/processEnv.setup.ts | 15 + .../tests/runtimes/browser/run.test.tsx | 38 -- .../tests/runtimes/browser/vitest.config.mts | 65 +++ .../tests/sandbox/commands/kill.test.ts | 4 +- .../commands/sandboxKilledDuringRun.test.ts | 4 +- packages/js-sdk/tests/sandbox/connect.test.ts | 9 +- .../js-sdk/tests/sandbox/files/read.test.ts | 60 +- .../js-sdk/tests/sandbox/files/write.test.ts | 5 +- packages/js-sdk/tests/sandbox/git/helpers.ts | 4 +- packages/js-sdk/tests/sandbox/host.test.ts | 6 +- .../tests/sandbox/lifecyclePayload.test.ts | 12 +- packages/js-sdk/tests/sandbox/network.test.ts | 11 +- .../js-sdk/tests/sandbox/pty/kill.test.ts | 4 +- .../tests/sandbox/pty/ptyConnect.test.ts | 4 +- .../tests/sandbox/pty/ptyCreate.test.ts | 2 +- .../js-sdk/tests/sandbox/pty/resize.test.ts | 4 +- .../tests/sandbox/pty/sendInput.test.ts | 5 +- packages/js-sdk/tests/sandbox/secure.test.ts | 31 +- .../js-sdk/tests/sandbox/snapshot.test.ts | 16 +- packages/js-sdk/tests/sandbox/timeout.test.ts | 22 +- packages/js-sdk/tests/sandbox/urls.test.ts | 10 +- packages/js-sdk/tests/setup.ts | 35 ++ packages/js-sdk/vitest.config.mts | 18 - pnpm-lock.yaml | 540 +----------------- 35 files changed, 355 insertions(+), 780 deletions(-) create mode 100644 packages/js-sdk/tests/runtimes/browser/importMeta.d.ts create mode 100644 packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts create mode 100644 packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts delete mode 100644 packages/js-sdk/tests/runtimes/browser/run.test.tsx create mode 100644 packages/js-sdk/tests/runtimes/browser/vitest.config.mts diff --git a/.github/workflows/js_sdk_tests.yml b/.github/workflows/js_sdk_tests.yml index 3a08f0dcb8..c1a4396c9b 100644 --- a/.github/workflows/js_sdk_tests.yml +++ b/.github/workflows/js_sdk_tests.yml @@ -8,7 +8,7 @@ on: type: string default: '' node-only: - description: 'Run only the Node legs (skip Bun, Deno, and Cloudflare)' + description: 'Run only the Node legs (skip Bun, Deno, Cloudflare, and the browser)' required: false type: boolean default: false @@ -29,14 +29,17 @@ jobs: strategy: fail-fast: false # With node-only (set by staging callers) the matrix collapses to the - # Node legs: the Bun/Deno/Cloudflare legs re-run suites the Node legs - # already cover and add sandbox/build load without extra backend - # signal, so the other runtimes are exercised against production only. + # Node legs: the Bun/Deno/Cloudflare/browser legs re-run suites the Node + # legs already cover and add sandbox/build load, so the other runtimes + # are exercised against production only. Note this does drop backend + # signal for the browser leg specifically — it's the only one that + # depends on the API's CORS headers — so a staging-only CORS regression + # surfaces on the production run rather than before merge. matrix: include: >- ${{ inputs.node-only && fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}]') - || fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}, {"runtime": "bun", "os": "ubuntu-22.04"}, {"runtime": "deno", "os": "ubuntu-22.04"}, {"runtime": "cloudflare", "os": "ubuntu-22.04"}, {"runtime": "cloudflare-deploy", "os": "ubuntu-22.04"}]') }} + || fromJSON('[{"runtime": "node", "os": "ubuntu-22.04"}, {"runtime": "node", "os": "windows-latest"}, {"runtime": "bun", "os": "ubuntu-22.04"}, {"runtime": "deno", "os": "ubuntu-22.04"}, {"runtime": "cloudflare", "os": "ubuntu-22.04"}, {"runtime": "cloudflare-deploy", "os": "ubuntu-22.04"}, {"runtime": "browser", "os": "ubuntu-22.04"}]') }} runs-on: ${{ matrix.os }} # The cloudflare-deploy leg is advisory: it deploys to a brand-new # Cloudflare preview account on every run, so it inherits that account's @@ -82,22 +85,21 @@ jobs: run: | pnpm install --frozen-lockfile - # Only the Node runtime runs the vitest `browser` project, which drives - # Chromium through Playwright. + # Only the browser leg needs Chromium, which Playwright drives. - name: Get Playwright version - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' id: playwright-version run: echo "version=$(node -p "require('playwright/package.json').version")" >> "$GITHUB_OUTPUT" - name: Cache Playwright browsers - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ matrix.os == 'windows-latest' && '~/AppData/Local/ms-playwright' || '~/.cache/ms-playwright' }} + path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - name: Install Playwright Chromium - if: matrix.runtime == 'node' + if: matrix.runtime == 'browser' run: pnpm run playwright:install # The unit bundle test and the Cloudflare deploy config fail in CI when @@ -153,3 +155,13 @@ jobs: env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + + # Full unit + connectionConfig suite inside Chromium (vitest browser mode). + # Chromium comes from the cached install step above; `pretest:browser` + # covers local runs, where it's a no-op once installed. + - name: Run test suite in the browser + if: matrix.runtime == 'browser' + run: pnpm test:browser + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} diff --git a/packages/js-sdk/.gitignore b/packages/js-sdk/.gitignore index bd580672f7..e9399a4d06 100644 --- a/packages/js-sdk/.gitignore +++ b/packages/js-sdk/.gitignore @@ -45,4 +45,6 @@ deno.lock .deploy-output.json .wrangler +# failure screenshots from the browser suite (`pnpm test:browser`) __screenshots__ +.vitest-attachments diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index c39a846520..d17af19bb0 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -35,6 +35,8 @@ "generate:mcp": "json2ts -i ./../../spec/mcp-server.json -o src/sandbox/mcp.d.ts --unreachableDefinitions --style.singleQuote --no-style.semi", "check-deps": "knip", "playwright:install": "playwright install chromium", + "pretest:browser": "pnpm run playwright:install", + "test:browser": "vitest run --config tests/runtimes/browser/vitest.config.mts", "test:bun": "bunx --bun vitest run --project unit --project connectionConfig --project template", "test:cf": "vitest run --config tests/runtimes/cloudflare/vitest.config.mts", "test:cf:deploy": "vitest run --config tests/runtimes/cloudflare-deploy/vitest.config.mts", @@ -46,13 +48,9 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.18.7", "@redocly/cli": "2.46.1", - "@testing-library/react": "^16.3.2", "@types/node": "^20.19.19", "@types/platform": "^1.3.6", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.4", "@typescript/native": "npm:typescript@^7.0.2", - "@vitejs/plugin-react": "^4.3.4", "@vitest/browser": "^4.1.10", "@vitest/browser-playwright": "^4.1.10", "dotenv": "^16.4.5", @@ -63,12 +61,9 @@ "npm-run-all": "^4.1.5", "openapi-typescript": "^7.13.0", "playwright": "^1.62.1", - "react": "^19.2.8", - "react-dom": "^19.2.8", "tsdown": "^0.22.14", "typescript": "npm:@typescript/typescript6@^6.0.2", "vitest": "^4.1.10", - "vitest-browser-react": "^2.2.0", "wrangler": "^4.123.0" }, "files": [ diff --git a/packages/js-sdk/tests/api/http2.test.ts b/packages/js-sdk/tests/api/http2.test.ts index 262bf96bf3..3238e16b72 100644 --- a/packages/js-sdk/tests/api/http2.test.ts +++ b/packages/js-sdk/tests/api/http2.test.ts @@ -3,8 +3,6 @@ import { afterEach, expect, test, vi } from 'vitest' afterEach(() => { vi.restoreAllMocks() vi.resetModules() - vi.doUnmock('undici') - vi.doUnmock('../../src/utils') delete process.env.E2B_API_CONNECTIONS delete process.env.E2B_API_INFLIGHT_REQUESTS }) diff --git a/packages/js-sdk/tests/api/list.test.ts b/packages/js-sdk/tests/api/list.test.ts index 52cb8a9f24..601cc17df9 100644 --- a/packages/js-sdk/tests/api/list.test.ts +++ b/packages/js-sdk/tests/api/list.test.ts @@ -1,8 +1,7 @@ import { assert } from 'vitest' -import { randomUUID } from 'crypto' import { Sandbox, SandboxInfo } from '../../src' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, canReadPaginationToken } from '../setup.js' sandboxTest.skipIf(isDebug)( 'list sandboxes', @@ -20,7 +19,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = randomUUID() + const uniqueId = crypto.randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { @@ -86,7 +85,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -122,7 +121,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await sandbox.betaPause() @@ -161,7 +160,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate running and paused sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandbox @@ -316,7 +315,7 @@ sandboxTest.skipIf(isDebug)( ) sandboxTest.skipIf(isDebug)('list sandboxes with filter', async () => { - const uniqueId = randomUUID() + const uniqueId = crypto.randomUUID() const extraSbx = await Sandbox.create({ metadata: { uniqueId } }) try { @@ -382,7 +381,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -418,7 +417,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await Sandbox.betaPause(sandbox.sandboxId) @@ -457,7 +456,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate running and paused sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandbox diff --git a/packages/js-sdk/tests/api/snapshot.test.ts b/packages/js-sdk/tests/api/snapshot.test.ts index 828389d143..cf66a9a872 100644 --- a/packages/js-sdk/tests/api/snapshot.test.ts +++ b/packages/js-sdk/tests/api/snapshot.test.ts @@ -1,26 +1,32 @@ import { assert } from 'vitest' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../setup.js' import { Sandbox } from '../../src' -sandboxTest.skipIf(isDebug)('pause sandbox', async ({ sandbox }) => { - await Sandbox.pause(sandbox.sandboxId) - assert.isFalse( - await sandbox.isRunning(), - 'Sandbox should not be running after pause' - ) -}) +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( + 'pause sandbox', + async ({ sandbox }) => { + await Sandbox.pause(sandbox.sandboxId) + assert.isFalse( + await sandbox.isRunning(), + 'Sandbox should not be running after pause' + ) + } +) -sandboxTest.skipIf(isDebug)('resume sandbox', async ({ sandbox }) => { - await Sandbox.pause(sandbox.sandboxId) - assert.isFalse( - await sandbox.isRunning(), - 'Sandbox should not be running after pause' - ) +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( + 'resume sandbox', + async ({ sandbox }) => { + await Sandbox.pause(sandbox.sandboxId) + assert.isFalse( + await sandbox.isRunning(), + 'Sandbox should not be running after pause' + ) - await Sandbox.connect(sandbox.sandboxId) - assert.isTrue( - await sandbox.isRunning(), - 'Sandbox should be running after resume' - ) -}) + await Sandbox.connect(sandbox.sandboxId) + assert.isTrue( + await sandbox.isRunning(), + 'Sandbox should be running after resume' + ) + } +) diff --git a/packages/js-sdk/tests/connectionConfig.test.ts b/packages/js-sdk/tests/connectionConfig.test.ts index df826d9692..b2f85f5ebf 100644 --- a/packages/js-sdk/tests/connectionConfig.test.ts +++ b/packages/js-sdk/tests/connectionConfig.test.ts @@ -4,6 +4,7 @@ import { setupRequestController, wrapStreamWithConnectionCleanup, } from '../src/connectionConfig' +import { runtime } from '../src/utils' // Store original env vars to restore after tests let originalEnv: { [key: string]: string | undefined } @@ -60,21 +61,26 @@ test('api_url has correct priority', () => { assert.equal(config.apiUrl, 'http://localhost:8080') }) -test('sandbox_url defaults to stable sandbox host in production', () => { - delete process.env.E2B_SANDBOX_URL - delete process.env.E2B_DOMAIN - delete process.env.E2B_DEBUG - - const config = new ConnectionConfig() - - assert.equal( - config.getSandboxUrl('sbx-test', { - sandboxDomain: 'e2b.app', - envdPort: 49983, - }), - 'https://sandbox.e2b.app' - ) -}) +// The stable host is deliberately not used in a browser (CORS); the browser +// side of this branch is asserted in connectionConfig.browser.test.ts. +test.skipIf(runtime === 'browser')( + 'sandbox_url defaults to stable sandbox host in production', + () => { + delete process.env.E2B_SANDBOX_URL + delete process.env.E2B_DOMAIN + delete process.env.E2B_DEBUG + + const config = new ConnectionConfig() + + assert.equal( + config.getSandboxUrl('sbx-test', { + sandboxDomain: 'e2b.app', + envdPort: 49983, + }), + 'https://sandbox.e2b.app' + ) + } +) test('sandbox_direct_url keeps per-sandbox host in production', () => { delete process.env.E2B_SANDBOX_URL diff --git a/packages/js-sdk/tests/envd/http2.test.ts b/packages/js-sdk/tests/envd/http2.test.ts index 9ad6e9ee0f..d1c022e891 100644 --- a/packages/js-sdk/tests/envd/http2.test.ts +++ b/packages/js-sdk/tests/envd/http2.test.ts @@ -3,8 +3,6 @@ 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 diff --git a/packages/js-sdk/tests/runtime.test.ts b/packages/js-sdk/tests/runtime.test.ts index af2919c7a3..a8d1aaf7d7 100644 --- a/packages/js-sdk/tests/runtime.test.ts +++ b/packages/js-sdk/tests/runtime.test.ts @@ -1,12 +1,13 @@ import { afterEach, expect, test, vi } from 'vitest' -// The unit project also runs under Bun, Deno, and Cloudflare's workerd, where -// the host runtime's own unstubbable marker (globalThis.Bun / globalThis.Deno / -// the Cloudflare-Workers user agent) correctly wins detection — these -// scenarios only exist on a Node host. +// The unit project also runs under Bun, Deno, Cloudflare's workerd, and a real +// browser, where the host runtime's own unstubbable marker (globalThis.Bun / +// globalThis.Deno / the Cloudflare-Workers user agent / `window`) correctly +// wins detection — these scenarios only exist on a Node host. const isNodeHost = typeof (globalThis as any).Bun === 'undefined' && typeof (globalThis as any).Deno === 'undefined' && + typeof (globalThis as any).window === 'undefined' && (globalThis as any).navigator?.userAgent !== 'Cloudflare-Workers' afterEach(() => { diff --git a/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts b/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts new file mode 100644 index 0000000000..b9a3c72f76 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/importMeta.d.ts @@ -0,0 +1,7 @@ +// Vite exposes the runner's `env` config on `import.meta.env`, which the +// browser suite reads instead of `process.env`. The canonical types live in +// `vite/client`, but vite is only a transitive dependency here (via vitest) and +// isn't resolvable from this package, so declare the shape the suite uses. +interface ImportMeta { + readonly env: Record +} diff --git a/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts new file mode 100644 index 0000000000..e8ec18c2f3 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts @@ -0,0 +1,45 @@ +import { assert, test } from 'vitest' + +import { Sandbox } from '../../../src' +import { template } from '../../template' + +// Bundlers don't give browser apps a `process` unless asked to, so the SDK has +// to be fully configurable through explicit options — `getEnvVar` +// (src/api/metadata.ts) returns '' when `process` is missing rather than +// throwing. The rest of this suite runs against the `process.env` shim from +// processEnv.setup.ts, which would mask a regression that reintroduces a bare +// `process` read on the create path, so this test drops the shim entirely and +// drives a real sandbox the way a browser app has to. +// +// Config comes from `import.meta.env` (where vitest puts the config's `env`), +// which is also how a Vite app would hand its own build-time values over. +const apiKey = import.meta.env.E2B_API_KEY +const domain = import.meta.env.E2B_DOMAIN || undefined +const isDebug = import.meta.env.E2B_DEBUG !== undefined + +test.skipIf(isDebug)( + 'drives a sandbox with no process global', + async () => { + const shim = Reflect.getOwnPropertyDescriptor(globalThis, 'process') + Reflect.deleteProperty(globalThis, 'process') + assert.equal(typeof process, 'undefined') + + try { + const sandbox = await Sandbox.create(template, { apiKey, domain }) + + try { + await sandbox.files.write('hello.txt', 'Hello World') + assert.equal(await sandbox.files.read('hello.txt'), 'Hello World') + + const result = await sandbox.commands.run('echo "from the browser"') + assert.equal(result.exitCode, 0) + assert.equal(result.stdout.trim(), 'from the browser') + } finally { + await sandbox.kill() + } + } finally { + if (shim) Reflect.defineProperty(globalThis, 'process', shim) + } + }, + 60_000 +) diff --git a/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts b/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts new file mode 100644 index 0000000000..b59a0a9a46 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/processEnv.setup.ts @@ -0,0 +1,15 @@ +// Browsers have no `process`: `getEnvVar` (src/api/metadata.ts) resolves to '' +// there, so E2B_* can only reach a browser app through whatever shim its +// bundler injects. Provide that shim — the same thing workerd's nodejs_compat +// gives the Cloudflare suite for free — so the shared suites can configure the +// SDK from the environment like they do on every other runtime. +// +// `process.env` aliases Vite's `import.meta.env`, which is where vitest puts +// the config's `env` and what `vi.stubEnv` writes to in browser mode, so env +// reads and stubbing both behave as they do on Node. +// +// Only `env` is defined: `process.release` must stay absent or the SDK would +// detect the runtime as 'node' instead of 'browser'. Tests that need a +// browser without any `process` at all delete it themselves — see +// noProcessGlobal.test.ts. +Object.assign(globalThis, { process: { env: import.meta.env } }) diff --git a/packages/js-sdk/tests/runtimes/browser/run.test.tsx b/packages/js-sdk/tests/runtimes/browser/run.test.tsx deleted file mode 100644 index dda6780c35..0000000000 --- a/packages/js-sdk/tests/runtimes/browser/run.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, inject, test } from 'vitest' -import { render } from 'vitest-browser-react' -import React from 'react' -import { useEffect, useState } from 'react' - -import { Sandbox } from '../../../src' -import { template } from '../../template' - -function E2BTest() { - const [text, setText] = useState() - - useEffect(() => { - const getText = async () => { - const sandbox = await Sandbox.create(template, { - apiKey: inject('E2B_API_KEY'), - domain: inject('E2B_DOMAIN'), - }) - - try { - await sandbox.commands.run('echo "Hello World" > hello.txt') - const content = await sandbox.files.read('hello.txt') - setText(content) - } finally { - await sandbox.kill() - } - } - - getText() - }, []) - - return
{text}
-} -test('browser test', async () => { - const screen = await render() - await expect - .element(screen.getByText('Hello World'), { timeout: 30_000 }) - .toBeInTheDocument() -}, 40_000) diff --git a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts new file mode 100644 index 0000000000..e72e4197a5 --- /dev/null +++ b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts @@ -0,0 +1,65 @@ +import { playwright } from '@vitest/browser-playwright' +import { config } from 'dotenv' +import { defineConfig } from 'vitest/config' + +// Real env vars win over `.env`, matching dotenv's own precedence. +const env = { ...config().parsed, ...process.env } + +// Config the shared suites read from the environment. Everything the SDK takes +// is `E2B_`-prefixed; ENABLE_VOLUME_TESTS gates the volume suite. Forwarded by +// name rather than as the whole environment, which would inline every host +// variable into JS served to the browser. +const testEnv = Object.fromEntries( + Object.entries(env).filter( + ([name]) => name.startsWith('E2B_') || name === 'ENABLE_VOLUME_TESTS' + ) +) as Record + +// Runs the unit + connectionConfig projects (same coverage as test:bun / +// test:deno / test:cf) inside a real Chromium via Playwright, against src. +// Tests the browser physically can't run are skipped from the test files +// themselves — see the capability flags in tests/setup.ts — so they stay +// visible in the report instead of disappearing into the exclude list below. +export default defineConfig({ + test: { + name: 'browser', + include: [ + 'tests/**/*.test.ts', + // Browser-only suite; tests/runtimes/** is otherwise excluded below. + 'tests/runtimes/browser/**/*.test.ts', + ], + exclude: [ + // Other runtimes' suites, which run under their own configs. This + // suite's own browser-only tests are re-included above; a new runtime + // directory needs adding here. + 'tests/runtimes/cloudflare*/**', + 'tests/template/**', + // Inspects the host-built dist/index.mjs via node:fs, which the browser + // can never see; the Node unit project keeps running it. + 'tests/bundle/**', + // Resolves the `undici`/`undici8` packages off `process.versions.node`. + // The browser never takes that path — `createRuntimeFetch` late-binds + // the global fetch outside Node — so there is nothing to cover here. + 'tests/undici.test.ts', + // These mock the API with msw's `setupServer`, whose `msw/node` entry + // pulls in node:http and can't be served to the browser. Porting them + // means `setupWorker` plus a service worker served from a public dir. + 'tests/sandbox/abortSignal.test.ts', + 'tests/volume/volume.test.ts', + ], + globals: false, + testTimeout: 30_000, + // A real browser has no `process`; the setup file shims `process.env` onto + // `import.meta.env`, where vitest puts `env` below, so the shared suites + // can read their config the way they do on every other runtime. + setupFiles: ['tests/runtimes/browser/processEnv.setup.ts'], + env: testEnv, + browser: { + enabled: true, + // Defaults to `isCI`, so set it explicitly for local runs too. + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}) diff --git a/packages/js-sdk/tests/sandbox/commands/kill.test.ts b/packages/js-sdk/tests/sandbox/commands/kill.test.ts index 4eddcc4460..036615169e 100644 --- a/packages/js-sdk/tests/sandbox/commands/kill.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/kill.test.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { ProcessExitError } from '../../../src/index.js' +import { CommandExitError } from '../../../src/index.js' import { sandboxTest } from '../../setup.js' @@ -10,7 +10,7 @@ sandboxTest('kill process', async ({ sandbox }) => { await sandbox.commands.kill(pid) await expect(sandbox.commands.run(`kill -0 ${pid}`)).rejects.toThrowError( - ProcessExitError + CommandExitError ) }) diff --git a/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts b/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts index 511a7a38a2..80e170db7b 100644 --- a/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts @@ -1,9 +1,9 @@ import { expect } from 'vitest' import { TimeoutError } from '../../../src/index.js' -import { sandboxTest, isDebug } from '../../setup.js' +import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../../setup.js' -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'killing the sandbox while a command is running throws an actionable error', async ({ sandbox }) => { const cmd = await sandbox.commands.run('sleep 60', { background: true }) diff --git a/packages/js-sdk/tests/sandbox/connect.test.ts b/packages/js-sdk/tests/sandbox/connect.test.ts index 9b7ca5f5f6..02f221b64f 100644 --- a/packages/js-sdk/tests/sandbox/connect.test.ts +++ b/packages/js-sdk/tests/sandbox/connect.test.ts @@ -1,7 +1,12 @@ import { assert, test, expect, vi } from 'vitest' import { Sandbox } from '../../src' -import { isDebug, sandboxTest, template } from '../setup.js' +import { + isDebug, + sandboxTest, + template, + canObserveStoppedSandbox, +} from '../setup.js' test('connect in debug mode does not call the API', async () => { const fetchSpy = vi.fn(() => { @@ -42,7 +47,7 @@ test.skipIf(isDebug)('connect', async () => { } }) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'connect resumes paused sandbox', async ({ sandbox }) => { await sandbox.pause() diff --git a/packages/js-sdk/tests/sandbox/files/read.test.ts b/packages/js-sdk/tests/sandbox/files/read.test.ts index 85b8f45b4e..d8315402c8 100644 --- a/packages/js-sdk/tests/sandbox/files/read.test.ts +++ b/packages/js-sdk/tests/sandbox/files/read.test.ts @@ -38,11 +38,12 @@ sandboxTest('read file as stream', async ({ sandbox }) => { await sandbox.files.write(filename, content) const stream = await sandbox.files.read(filename, { format: 'stream' }) - const chunks: Uint8Array[] = [] + const decoder = new TextDecoder() + let readContent = '' for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) + readContent += decoder.decode(chunk, { stream: true }) } - const readContent = Buffer.concat(chunks).toString('utf-8') + readContent += decoder.decode() assert.equal(readContent, content) }) @@ -71,56 +72,9 @@ sandboxTest('read empty file in all formats', async ({ sandbox }) => { const stream = await sandbox.files.read(filename, { format: 'stream' }) expect(stream).toBeInstanceOf(ReadableStream) - const chunks: Uint8Array[] = [] + let bytesRead = 0 for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) + bytesRead += chunk.length } - expect(Buffer.concat(chunks).length).toBe(0) -}) - -sandboxTest('read file as stream', async ({ sandbox }) => { - const filename = 'test_read_stream.txt' - const content = 'Streamed read content. '.repeat(10_000) - - await sandbox.files.write(filename, content) - const stream = await sandbox.files.read(filename, { format: 'stream' }) - - const chunks: Uint8Array[] = [] - for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) - } - const readContent = Buffer.concat(chunks).toString('utf-8') - assert.equal(readContent, content) -}) - -sandboxTest('read non-existing file as stream', async ({ sandbox }) => { - const filename = 'non_existing_file.txt' - - await expect( - sandbox.files.read(filename, { format: 'stream' }) - ).rejects.toThrowError(FileNotFoundError) -}) - -sandboxTest('read empty file in all formats', async ({ sandbox }) => { - const filename = 'empty-file-formats.txt' - await sandbox.commands.run(`touch ${filename}`) - - const text = await sandbox.files.read(filename, { format: 'text' }) - expect(text).toBe('') - - const bytes = await sandbox.files.read(filename, { format: 'bytes' }) - expect(bytes).toBeInstanceOf(Uint8Array) - expect(bytes.length).toBe(0) - - const blob = await sandbox.files.read(filename, { format: 'blob' }) - expect(blob).toBeInstanceOf(Blob) - expect(blob.size).toBe(0) - - const stream = await sandbox.files.read(filename, { format: 'stream' }) - expect(stream).toBeInstanceOf(ReadableStream) - const chunks: Uint8Array[] = [] - for await (const chunk of stream as unknown as AsyncIterable) { - chunks.push(chunk) - } - expect(Buffer.concat(chunks).length).toBe(0) + expect(bytesRead).toBe(0) }) diff --git a/packages/js-sdk/tests/sandbox/files/write.test.ts b/packages/js-sdk/tests/sandbox/files/write.test.ts index 61f046cfe0..5116006e3b 100644 --- a/packages/js-sdk/tests/sandbox/files/write.test.ts +++ b/packages/js-sdk/tests/sandbox/files/write.test.ts @@ -1,4 +1,3 @@ -import path from 'path' import { assert } from 'vitest' import { WriteEntry } from '../../../src/sandbox/filesystem' @@ -111,7 +110,7 @@ sandboxTest('write multiple files', async ({ sandbox }) => { const file = files[i] const info = infos[i] - assert.equal(info.name, path.basename(file.path)) + assert.equal(info.name, file.path.split('/').pop()) assert.equal(info.path, file.path) assert.equal(info.type, 'file') @@ -206,7 +205,7 @@ sandboxTest('writeFiles with multiple files', async ({ sandbox }) => { const file = files[i] const info = infos[i] - assert.equal(info.name, path.basename(file.path)) + assert.equal(info.name, file.path.split('/').pop()) assert.equal(info.path, `/home/user/${file.path}`) assert.equal(info.type, 'file') diff --git a/packages/js-sdk/tests/sandbox/git/helpers.ts b/packages/js-sdk/tests/sandbox/git/helpers.ts index 797682f05b..cb19c1d82a 100644 --- a/packages/js-sdk/tests/sandbox/git/helpers.ts +++ b/packages/js-sdk/tests/sandbox/git/helpers.ts @@ -1,5 +1,3 @@ -import { randomUUID } from 'node:crypto' - export const AUTHOR_NAME = 'Sandbox Bot' export const AUTHOR_EMAIL = 'sandbox@example.com' export const USERNAME = 'git' @@ -10,7 +8,7 @@ export const PROTOCOL = 'https' const BASE_DIR = '/tmp/test-git' export async function createBaseDir(sandbox: any) { - const baseDir = `${BASE_DIR}/${randomUUID()}` + const baseDir = `${BASE_DIR}/${crypto.randomUUID()}` await sandbox.commands.run(`rm -rf "${baseDir}" && mkdir -p "${baseDir}"`) return baseDir } diff --git a/packages/js-sdk/tests/sandbox/host.test.ts b/packages/js-sdk/tests/sandbox/host.test.ts index 37e45f68c8..47045b7e8c 100644 --- a/packages/js-sdk/tests/sandbox/host.test.ts +++ b/packages/js-sdk/tests/sandbox/host.test.ts @@ -1,8 +1,8 @@ import { assert } from 'vitest' -import { isDebug, sandboxTest, wait } from '../setup.js' +import { isDebug, sandboxTest, wait, canFetchSandboxServers } from '../setup.js' import { catchCmdExitErrorInBackground } from '../cmdHelper.js' -sandboxTest( +sandboxTest.skipIf(!canFetchSandboxServers)( 'ping server in running sandbox', async ({ sandbox }) => { const cmd = await sandbox.commands.run('python -m http.server 8000', { @@ -39,7 +39,7 @@ sandboxTest( 60_000 ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( 'ping server in non-running sandbox', async ({ sandbox }) => { const host = sandbox.getHost(3000) diff --git a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts index 28a1999b7a..8eee24560b 100644 --- a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts @@ -1,7 +1,13 @@ import { assert, expect, test } from 'vitest' import { InvalidArgumentError, Sandbox } from '../../src' -import { isDebug, template, wait } from '../setup.js' +import { + isDebug, + template, + wait, + canObserveStoppedSandbox, + canFetchSandboxServers, +} from '../setup.js' test.skipIf(isDebug)( 'filesystem-only auto-pause cannot be combined with auto-resume', @@ -38,7 +44,7 @@ test.skipIf(isDebug)( } ) -test.skipIf(isDebug)( +test.skipIf(isDebug || !canObserveStoppedSandbox)( 'auto-pause without auto-resume requires connect to wake', async () => { const sandbox = await Sandbox.create(template, { @@ -111,7 +117,7 @@ test.skipIf(isDebug)( 60_000 ) -test.skipIf(isDebug)( +test.skipIf(isDebug || !canFetchSandboxServers)( 'auto-resume wakes paused sandbox on http request', async () => { const sandbox = await Sandbox.create(template, { diff --git a/packages/js-sdk/tests/sandbox/network.test.ts b/packages/js-sdk/tests/sandbox/network.test.ts index 82a8f20003..273e4dec3a 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -1,7 +1,12 @@ import { assert, expect, describe } from 'vitest' import { CommandExitError, Sandbox } from '../../src' -import { sandboxTest, isDebug, template } from '../setup.js' +import { + sandboxTest, + isDebug, + template, + canFetchSandboxServers, +} from '../setup.js' import { httpbinTemplate } from '../template.js' describe('allow only 1.1.1.1', () => { @@ -130,7 +135,7 @@ describe('allowPublicTraffic=false', () => { }, }) - sandboxTest.skipIf(isDebug)( + sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( 'sandbox requires traffic access token', async ({ sandbox }) => { // Verify the sandbox was created successfully and has a traffic access token @@ -172,7 +177,7 @@ describe('allowPublicTraffic=true', () => { }, }) - sandboxTest.skipIf(isDebug)( + sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( 'sandbox works without token', async ({ sandbox }) => { // Start a simple HTTP server in the sandbox diff --git a/packages/js-sdk/tests/sandbox/pty/kill.test.ts b/packages/js-sdk/tests/sandbox/pty/kill.test.ts index f602515770..d0693f7a41 100644 --- a/packages/js-sdk/tests/sandbox/pty/kill.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/kill.test.ts @@ -1,6 +1,6 @@ import { sandboxTest } from '../../setup' import { assert, expect } from 'vitest' -import { ProcessExitError } from '../../../src/index.js' +import { CommandExitError } from '../../../src/index.js' sandboxTest('kill PTY', async ({ sandbox }) => { const terminal = await sandbox.pty.create({ @@ -15,7 +15,7 @@ sandboxTest('kill PTY', async ({ sandbox }) => { // The PTY process should no longer be running. await expect( sandbox.commands.run(`kill -0 ${terminal.pid}`) - ).rejects.toThrowError(ProcessExitError) + ).rejects.toThrowError(CommandExitError) }) sandboxTest('kill non-existing PTY', async ({ sandbox }) => { diff --git a/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts b/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts index 903da6d105..6ba72d6b6f 100644 --- a/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/ptyConnect.test.ts @@ -18,7 +18,7 @@ sandboxTest('pty connect/reconnect', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $FOO\n')) + new TextEncoder().encode('echo $FOO\n') ) // Give time for the command output in the first connection @@ -35,7 +35,7 @@ sandboxTest('pty connect/reconnect', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $FOO\nexit\n')) + new TextEncoder().encode('echo $FOO\nexit\n') ) await reconnectHandle.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts b/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts index 5a0b98c589..d77db612c0 100644 --- a/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/ptyCreate.test.ts @@ -17,7 +17,7 @@ sandboxTest('create PTY', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('echo $ABC\nexit\n')) + new TextEncoder().encode('echo $ABC\nexit\n') ) await terminal.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/resize.test.ts b/packages/js-sdk/tests/sandbox/pty/resize.test.ts index 21bb878298..143182b6d8 100644 --- a/packages/js-sdk/tests/sandbox/pty/resize.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/resize.test.ts @@ -16,7 +16,7 @@ sandboxTest('resize', async ({ sandbox }) => { await sandbox.pty.sendInput( terminal.pid, - new Uint8Array(Buffer.from('tput cols\nexit\n')) + new TextEncoder().encode('tput cols\nexit\n') ) await terminal.wait() @@ -33,7 +33,7 @@ sandboxTest('resize', async ({ sandbox }) => { await sandbox.pty.resize(resizedTerminal.pid, { cols: 100, rows: 24 }) await sandbox.pty.sendInput( resizedTerminal.pid, - new Uint8Array(Buffer.from('tput cols\nexit\n')) + new TextEncoder().encode('tput cols\nexit\n') ) await resizedTerminal.wait() diff --git a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts index b32d7f23dd..c64c2fbee5 100644 --- a/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts +++ b/packages/js-sdk/tests/sandbox/pty/sendInput.test.ts @@ -8,10 +8,7 @@ sandboxTest('send input', async ({ sandbox }) => { onData: () => null, }) - await sandbox.pty.sendInput( - terminal.pid, - new Uint8Array(Buffer.from('exit\n')) - ) + await sandbox.pty.sendInput(terminal.pid, new TextEncoder().encode('exit\n')) await terminal.wait() expect(terminal.exitCode).toBe(0) diff --git a/packages/js-sdk/tests/sandbox/secure.test.ts b/packages/js-sdk/tests/sandbox/secure.test.ts index 90a8712941..e8647031fd 100644 --- a/packages/js-sdk/tests/sandbox/secure.test.ts +++ b/packages/js-sdk/tests/sandbox/secure.test.ts @@ -1,7 +1,20 @@ import { assert, test, describe } from 'vitest' import { getSignature, Sandbox } from '../../src' import { sandboxTest, isDebug } from '../setup' -import { randomUUID, createHash } from 'node:crypto' + +/** + * Recomputes the signature envd expects, independently of the SDK's own + * implementation. WebCrypto rather than node:crypto so the expectation is + * computed the same way on every runtime the suite covers. + */ +async function expectedSignature(raw: string): Promise { + const digest = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(raw) + ) + const base64 = btoa(String.fromCharCode(...new Uint8Array(digest))) + return 'v1_' + base64.replace(/=+$/, '') +} describe('secure sandbox', () => { sandboxTest.override({ @@ -40,16 +53,12 @@ test.skipIf(isDebug)('signing generation', async () => { const operation = 'read' const path = '/home/user/hello.txt' const user = 'root' - const envdAccessToken = randomUUID() + const envdAccessToken = crypto.randomUUID() const signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}` - const buff = Buffer.from(signatureRaw, 'utf8') - const hash = createHash('sha256').update(buff).digest() - const signature = 'v1_' + hash.toString('base64').replace(/=+$/, '') - const readSignatureExpected = { - signature: signature, + signature: await expectedSignature(signatureRaw), expiration: null, } @@ -67,7 +76,7 @@ test.skipIf(isDebug)('signing generation with expiration', async () => { const operation = 'read' const path = '/home/user/hello.txt' const user = 'root' - const envdAccessToken = randomUUID() + const envdAccessToken = crypto.randomUUID() const expirationInSeconds = 120 const signatureExpiration = expirationInSeconds @@ -75,12 +84,8 @@ test.skipIf(isDebug)('signing generation with expiration', async () => { : null const signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}:${signatureExpiration?.toString()}` - const buff = Buffer.from(signatureRaw, 'utf8') - const hash = createHash('sha256').update(buff).digest() - const signature = 'v1_' + hash.toString('base64').replace(/=+$/, '') - const readSignatureExpected = { - signature: signature, + signature: await expectedSignature(signatureRaw), expiration: signatureExpiration, } diff --git a/packages/js-sdk/tests/sandbox/snapshot.test.ts b/packages/js-sdk/tests/sandbox/snapshot.test.ts index ac0dead40d..8b55bc47e8 100644 --- a/packages/js-sdk/tests/sandbox/snapshot.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot.test.ts @@ -1,8 +1,8 @@ import { assert, describe } from 'vitest' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../setup.js' -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox', async ({ sandbox }) => { assert.isTrue(await sandbox.isRunning()) @@ -25,7 +25,7 @@ describe('pause and resume with env vars', () => { }, }) - sandboxTest.skipIf(isDebug)( + sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox with env vars', async ({ sandbox }) => { // Environment variables of a process exist at runtime, and are not stored in some file or so. @@ -52,7 +52,7 @@ describe('pause and resume with env vars', () => { ) }) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox with file', async ({ sandbox }) => { const filename = 'test_snapshot.txt' @@ -81,7 +81,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox with ongoing long running process', async ({ sandbox }) => { const cmd = await sandbox.commands.run('sleep 3600', { background: true }) @@ -105,7 +105,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox with completed long running process', async ({ sandbox }) => { const filename = 'test_long_running.txt' @@ -137,7 +137,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'pause and resume a sandbox with http server', async ({ sandbox }) => { await sandbox.commands.run('python3 -m http.server 8000', { @@ -163,7 +163,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( 'filesystem-only pause reboots on resume but keeps the filesystem', async ({ sandbox }) => { // Absolute path: a cold boot may not restore the template's default diff --git a/packages/js-sdk/tests/sandbox/timeout.test.ts b/packages/js-sdk/tests/sandbox/timeout.test.ts index 249666ada3..29881c5fc9 100644 --- a/packages/js-sdk/tests/sandbox/timeout.test.ts +++ b/packages/js-sdk/tests/sandbox/timeout.test.ts @@ -1,14 +1,22 @@ import { expect } from 'vitest' -import { sandboxTest, isDebug, wait } from '../setup.js' - -sandboxTest.skipIf(isDebug)('shorten timeout', async ({ sandbox }) => { - await sandbox.setTimeout(5000) +import { + sandboxTest, + isDebug, + wait, + canObserveStoppedSandbox, +} from '../setup.js' + +sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( + 'shorten timeout', + async ({ sandbox }) => { + await sandbox.setTimeout(5000) - await wait(6000) + await wait(6000) - expect(await sandbox.isRunning()).toBeFalsy() -}) + expect(await sandbox.isRunning()).toBeFalsy() + } +) sandboxTest.skipIf(isDebug)( 'shorten then lengthen timeout', diff --git a/packages/js-sdk/tests/sandbox/urls.test.ts b/packages/js-sdk/tests/sandbox/urls.test.ts index f20854e75f..7fa6819356 100644 --- a/packages/js-sdk/tests/sandbox/urls.test.ts +++ b/packages/js-sdk/tests/sandbox/urls.test.ts @@ -1,6 +1,7 @@ import { assert, describe, test } from 'vitest' import { getSignature, InvalidArgumentError, Sandbox } from '../../src' +import { runtime } from '../../src/utils' import { TEST_API_KEY } from '../setup' function createSandbox(envdAccessToken?: string) { @@ -19,7 +20,14 @@ describe('sandbox file URLs', () => { test('file URLs use direct sandbox host when envd API uses stable host', async () => { const sandbox = createSandbox() - assert.equal(sandbox['envdApiUrl'], 'https://sandbox.e2b.app') + // The stable host is deliberately not used in a browser (CORS), which + // reaches envd on the direct host — file URLs use the direct host either way. + assert.equal( + sandbox['envdApiUrl'], + runtime === 'browser' + ? 'https://49983-sandbox-id.e2b.app' + : 'https://sandbox.e2b.app' + ) assert.equal(sandbox['envdDirectUrl'], 'https://49983-sandbox-id.e2b.app') assert.equal( await sandbox.downloadUrl('/tmp/a.txt'), diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 91d427c3cc..a71ab9372b 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -8,6 +8,7 @@ import { TemplateClass, Volume, } from '../src' +import { runtime } from '../src/utils' import { template } from './template' interface SandboxFixture { @@ -148,6 +149,40 @@ export const isDebug = process.env.E2B_DEBUG !== undefined /** Placeholder API key with a valid format for tests that don't hit the API. */ export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` +/** + * Whether the test can read a response from a server running inside a sandbox. + * + * A browser only exposes a cross-origin response to JS when the server opts in + * with CORS headers. Servers a test starts in a sandbox (`python -m + * http.server`, and the sandbox proxy's own 502 page) don't, so there the fetch + * fails as an opaque `TypeError: Failed to fetch` no matter what the server + * answered. Nothing the SDK can paper over — a real browser app would hit the + * same wall. + */ +export const canFetchSandboxServers = runtime !== 'browser' + +/** + * Whether the test can read the pagination cursor the API returns. + * + * `Paginator` takes its cursor from the `x-next-token` response header, and the + * API doesn't name that header in `Access-Control-Expose-Headers` — so a + * browser withholds it from JS and every page looks like the last one. + * Paginating with a `limit` can't work there until the API exposes the header; + * the SDK can't reach around CORS. + */ +export const canReadPaginationToken = runtime !== 'browser' + +/** + * Whether the test can observe a sandbox that has stopped. + * + * `isRunning()` reads envd's `/health` over the sandbox host, and a stopped + * sandbox answers 502 from the edge without CORS headers — so in a browser the + * probe rejects instead of reporting `false`. That also costs the health-check + * refinement in `handleRpcErrorWithHealthCheck`: a connection dropped by a kill + * stays a generic `SandboxError` there instead of a `TimeoutError`. + */ +export const canObserveStoppedSandbox = runtime !== 'browser' + function generateRandomString(length: number = 8): string { return Math.random() .toString(36) diff --git a/packages/js-sdk/vitest.config.mts b/packages/js-sdk/vitest.config.mts index 63439b0112..77c2fba23c 100644 --- a/packages/js-sdk/vitest.config.mts +++ b/packages/js-sdk/vitest.config.mts @@ -1,5 +1,4 @@ import { defineConfig } from 'vitest/config' -import { playwright } from '@vitest/browser-playwright' import { config } from 'dotenv' const env = config() @@ -36,23 +35,6 @@ export default defineConfig({ }, }, }, - { - test: { - name: 'browser', - include: ['tests/runtimes/browser/**/*.{test,spec}.tsx'], - browser: { - enabled: true, - headless: true, - instances: [{ browser: 'chromium' }], - provider: playwright(), - // https://playwright.dev - }, - provide: { - E2B_API_KEY: process.env.E2B_API_KEY || env.parsed?.E2B_API_KEY, - E2B_DOMAIN: process.env.E2B_DOMAIN || env.parsed?.E2B_DOMAIN, - }, - }, - }, { test: { name: 'template', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33f613d52f..f8a9671b62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,27 +178,15 @@ importers: '@redocly/cli': specifier: 2.46.1 version: 2.46.1 - '@testing-library/react': - specifier: ^16.3.2 - version: 16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@types/node': specifier: ^20.19.19 version: 20.19.43 '@types/platform': specifier: ^1.3.6 version: 1.3.6 - '@types/react': - specifier: ^19.2.18 - version: 19.2.18 - '@types/react-dom': - specifier: ^19.2.4 - version: 19.2.4(@types/react@19.2.18) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.3.4(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) '@vitest/browser': specifier: ^4.1.10 version: 4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10) @@ -229,12 +217,6 @@ importers: playwright: specifier: ^1.62.1 version: 1.62.1 - react: - specifier: ^19.2.8 - version: 19.2.8 - react-dom: - specifier: ^19.2.8 - version: 19.2.8(react@19.2.8) tsdown: specifier: ^0.22.14 version: 0.22.14(@typescript/typescript6@6.0.2) @@ -244,9 +226,6 @@ importers: vitest: specifier: ^4.1.10 version: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) - vitest-browser-react: - specifier: ^2.2.0 - version: 2.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10) wrangler: specifier: ^4.123.0 version: 4.123.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) @@ -267,119 +246,23 @@ packages: resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.7': - resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.7': - resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.29.7': - resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.29.7': - resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.29.7': - resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.29.7': - resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - - '@babel/helper-plugin-utils@7.26.5': - resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.7': - resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.7': - resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.29.7': - resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.7': - resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.25.8': - resolution: {integrity: sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.2': - resolution: {integrity: sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.25.9': - resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - - '@babel/plugin-transform-react-jsx-source@7.25.9': - resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': - resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.25.8': - resolution: {integrity: sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.1': - resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -1377,9 +1260,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1872,40 +1752,6 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@testing-library/dom@10.4.0': - resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} - engines: {node: '>=18'} - - '@testing-library/react@16.3.2': - resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} - engines: {node: '>=18'} - peerDependencies: - '@testing-library/dom': ^10.0.0 - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1943,14 +1789,6 @@ packages: '@types/platform@1.3.6': resolution: {integrity: sha512-ZmSaqHuvzv+jC232cFoz2QqPUkaj6EvMmCrWcx3WRr7xTPVFCMUOTcOq8m2d+Zw1iKRc1kDiaA+jtNrV0hkVew==} - '@types/react-dom@19.2.4': - resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.18': - resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} - '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -2084,12 +1922,6 @@ packages: resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} hasBin: true - '@vitejs/plugin-react@4.3.4': - resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^6.4.3 - '@vitest/browser-playwright@4.1.10': resolution: {integrity: sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==} peerDependencies: @@ -2310,10 +2142,6 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -2331,9 +2159,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -2393,11 +2218,6 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2425,9 +2245,6 @@ packages: resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} engines: {node: '>=14.16'} - caniuse-lite@1.0.30001753: - resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -2533,9 +2350,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2575,10 +2389,6 @@ packages: defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -2594,9 +2404,6 @@ packages: dockerfile-ast@0.7.1: resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -2620,9 +2427,6 @@ packages: easy-table@1.2.0: resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} - electron-to-chromium@1.5.80: - resolution: {integrity: sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2789,10 +2593,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3137,11 +2937,6 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true - jsesc@3.0.2: - resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} - engines: {node: '>=6'} - hasBin: true - json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} @@ -3160,11 +2955,6 @@ packages: json2md@2.0.3: resolution: {integrity: sha512-ZPzh6Djvqz8grJMxKllfCHo0p+p7BsbZ1J95KcCJgvvfdoy7myuKrrkUp80Kpy+wGauykC0dYljLqLY0kENaOw==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -3202,13 +2992,6 @@ packages: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3313,9 +3096,6 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3546,10 +3326,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - pretty-ms@9.1.0: resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} engines: {node: '>=18'} @@ -3570,22 +3346,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.2.8: - resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} - peerDependencies: - react: ^19.2.8 - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - - react@19.2.8: - resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} - engines: {node: '>=0.10.0'} - read-pkg@3.0.0: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} @@ -3685,17 +3445,10 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -3961,10 +3714,6 @@ packages: resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} hasBin: true - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4092,12 +3841,6 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - update-browserslist-db@1.1.2: - resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} @@ -4156,20 +3899,6 @@ packages: yaml: optional: true - vitest-browser-react@2.2.0: - resolution: {integrity: sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==} - peerDependencies: - '@types/react': ^18.0.0 || ^19.0.0 - '@types/react-dom': ^18.0.0 || ^19.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - vitest: ^4.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -4320,9 +4049,6 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4394,136 +4120,16 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.7': {} - - '@babel/core@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@10.2.2) - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.0.2 - - '@babel/helper-compilation-targets@7.29.7': - dependencies: - '@babel/compat-data': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-module-imports@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.26.5': {} - - '@babel/helper-string-parser@7.25.7': {} - - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.25.7': {} - - '@babel/helper-validator-identifier@7.27.1': {} - '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.29.7': {} - - '@babel/helpers@7.29.7': - dependencies: - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.25.8': - dependencies: - '@babel/types': 7.27.1 - - '@babel/parser@7.27.2': - dependencies: - '@babel/types': 7.27.1 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.26.5 - - '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.26.5 - '@babel/runtime@7.29.7': {} - '@babel/template@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@10.2.2) - transitivePeerDependencies: - - supports-color - - '@babel/types@7.25.8': - dependencies: - '@babel/helper-string-parser': 7.25.7 - '@babel/helper-validator-identifier': 7.25.7 - to-fast-properties: 2.0.0 - - '@babel/types@7.27.1': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -5316,11 +4922,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/resolve-uri@3.1.2': {} @@ -5668,50 +5270,6 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@testing-library/dom@10.4.0': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/runtime': 7.29.7 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - - '@testing-library/react@16.3.2(@testing-library/dom@10.4.0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': - dependencies: - '@babel/runtime': 7.29.7 - '@testing-library/dom': 10.4.0 - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - - '@types/aria-query@5.0.4': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.25.8 - '@babel/types': 7.25.8 - '@types/babel__generator': 7.6.8 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 - - '@types/babel__generator@7.6.8': - dependencies: - '@babel/types': 7.27.1 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.27.2 - '@babel/types': 7.27.1 - - '@types/babel__traverse@7.20.6': - dependencies: - '@babel/types': 7.27.1 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5746,14 +5304,6 @@ snapshots: '@types/platform@1.3.6': {} - '@types/react-dom@19.2.4(@types/react@19.2.18)': - dependencies: - '@types/react': 19.2.18 - - '@types/react@19.2.18': - dependencies: - csstype: 3.2.3 - '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 20.19.43 @@ -5828,17 +5378,6 @@ snapshots: dependencies: '@typescript/old': typescript@6.0.3 - '@vitejs/plugin-react@4.3.4(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))': - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.29.7) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0) - transitivePeerDependencies: - - supports-color - '@vitest/browser-playwright@4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(playwright@1.62.1)(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10)': dependencies: '@vitest/browser': 4.1.10(bufferutil@4.0.8)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.3)(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0))(vitest@4.1.10) @@ -6029,8 +5568,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} ansis@4.3.1: {} @@ -6043,10 +5580,6 @@ snapshots: argparse@2.0.1: {} - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -6116,13 +5649,6 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.24.4: - dependencies: - caniuse-lite: 1.0.30001753 - electron-to-chromium: 1.5.80 - node-releases: 2.0.19 - update-browserslist-db: 1.1.2(browserslist@4.24.4) - buffer-from@1.1.2: optional: true @@ -6152,8 +5678,6 @@ snapshots: camelcase@7.0.1: {} - caniuse-lite@1.0.30001753: {} - chai@6.2.2: {} chalk@2.4.2: @@ -6250,8 +5774,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.2.3: {} - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6301,8 +5823,6 @@ snapshots: defu@6.1.7: {} - dequal@2.0.3: {} - detect-indent@6.1.0: {} detect-libc@2.1.2: {} @@ -6316,8 +5836,6 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - dom-accessibility-api@0.5.16: {} - dotenv@16.4.5: {} dts-resolver@3.0.0: {} @@ -6336,8 +5854,6 @@ snapshots: optionalDependencies: wcwidth: 1.0.1 - electron-to-chromium@1.5.80: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -6604,8 +6120,6 @@ snapshots: functions-have-names@1.2.3: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} get-intrinsic@1.2.1: @@ -6940,8 +6454,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.0.2: {} - json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@5.0.0: {} @@ -6964,8 +6476,6 @@ snapshots: dependencies: indento: 1.1.13 - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -7012,12 +6522,6 @@ snapshots: lru-cache@11.5.2: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - lz-string@1.5.0: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7138,8 +6642,6 @@ snapshots: node-gyp-build@4.8.4: optional: true - node-releases@2.0.19: {} - normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -7354,12 +6856,6 @@ snapshots: prettier@3.6.2: {} - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 - pretty-ms@9.1.0: dependencies: parse-ms: 4.0.0 @@ -7374,17 +6870,6 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@19.2.8(react@19.2.8): - dependencies: - react: 19.2.8 - scheduler: 0.27.0 - - react-is@17.0.2: {} - - react-refresh@0.14.2: {} - - react@19.2.8: {} - read-pkg@3.0.0: dependencies: load-json-file: 4.0.0 @@ -7538,12 +7023,8 @@ snapshots: safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - semver@5.7.2: {} - semver@6.3.1: {} - semver@7.7.2: {} semver@7.8.5: {} @@ -7870,8 +7351,6 @@ snapshots: dependencies: tldts-core: 7.4.10 - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -8009,12 +7488,6 @@ snapshots: until-async@3.0.2: {} - update-browserslist-db@1.1.2(browserslist@4.24.4): - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 - uri-js-replace@1.0.1: {} utf-8-validate@6.0.3: @@ -8045,15 +7518,6 @@ snapshots: jiti: 2.4.2 terser: 5.46.0 - vitest-browser-react@2.2.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(vitest@4.1.10): - dependencies: - react: 19.2.8 - react-dom: 19.2.8(react@19.2.8) - vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)) - optionalDependencies: - '@types/react': 19.2.18 - '@types/react-dom': 19.2.4(@types/react@19.2.18) - vitest@4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.15.0(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@20.19.43)(jiti@2.4.2)(terser@5.46.0)): dependencies: '@vitest/expect': 4.1.10 @@ -8230,8 +7694,6 @@ snapshots: y18n@5.0.8: {} - yallist@3.1.1: {} - yallist@5.0.0: {} yaml-ast-parser@0.0.43: {} From 0ab365c54d745ed552bfedec7d095799313b3277 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:53:19 +0200 Subject: [PATCH 2/9] test(js-sdk): disable browser failure screenshots, drop pagination gate These tests drive the SDK and never render anything, so vitest's browser.screenshotFailures (on by default when headless) wrote a PNG of a blank page per failed test into .vitest-attachments/. Turned off, which also lets the .gitignore entry it needed go away. Removes canReadPaginationToken: the API not exposing X-Next-Token via Access-Control-Expose-Headers is fixed upstream in infra#3388, so the six limit-based pagination tests are un-gated and run in the browser like everywhere else. Note infra#3388 is not on production yet, and the browser leg only ever runs against production (staging callers pass node-only, which drops it), so those six tests fail on `paginator.hasNext` until the deploy lands. Co-Authored-By: Claude --- packages/js-sdk/.gitignore | 2 -- packages/js-sdk/tests/api/list.test.ts | 14 +++++++------- .../tests/runtimes/browser/vitest.config.mts | 4 ++++ packages/js-sdk/tests/setup.ts | 11 ----------- 4 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/js-sdk/.gitignore b/packages/js-sdk/.gitignore index e9399a4d06..bd580672f7 100644 --- a/packages/js-sdk/.gitignore +++ b/packages/js-sdk/.gitignore @@ -45,6 +45,4 @@ deno.lock .deploy-output.json .wrangler -# failure screenshots from the browser suite (`pnpm test:browser`) __screenshots__ -.vitest-attachments diff --git a/packages/js-sdk/tests/api/list.test.ts b/packages/js-sdk/tests/api/list.test.ts index 601cc17df9..e184b19eda 100644 --- a/packages/js-sdk/tests/api/list.test.ts +++ b/packages/js-sdk/tests/api/list.test.ts @@ -1,7 +1,7 @@ import { assert } from 'vitest' import { Sandbox, SandboxInfo } from '../../src' -import { sandboxTest, isDebug, canReadPaginationToken } from '../setup.js' +import { sandboxTest, isDebug } from '../setup.js' sandboxTest.skipIf(isDebug)( 'list sandboxes', @@ -85,7 +85,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -121,7 +121,7 @@ sandboxTest.skipIf(isDebug || !canReadPaginationToken)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await sandbox.betaPause() @@ -160,7 +160,7 @@ sandboxTest.skipIf(isDebug || !canReadPaginationToken)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate running and paused sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandbox @@ -381,7 +381,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -417,7 +417,7 @@ sandboxTest.skipIf(isDebug || !canReadPaginationToken)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await Sandbox.betaPause(sandbox.sandboxId) @@ -456,7 +456,7 @@ sandboxTest.skipIf(isDebug || !canReadPaginationToken)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate running and paused sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandbox diff --git a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts index e72e4197a5..f816c190d8 100644 --- a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts +++ b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts @@ -58,6 +58,10 @@ export default defineConfig({ enabled: true, // Defaults to `isCI`, so set it explicitly for local runs too. headless: true, + // These tests drive the SDK and never render anything, so a failure + // screenshot is a picture of a blank page. Off by default it would write + // one PNG per failed test into .vitest-attachments/. + screenshotFailures: false, provider: playwright(), instances: [{ browser: 'chromium' }], }, diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index a71ab9372b..a10484a058 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -161,17 +161,6 @@ export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` */ export const canFetchSandboxServers = runtime !== 'browser' -/** - * Whether the test can read the pagination cursor the API returns. - * - * `Paginator` takes its cursor from the `x-next-token` response header, and the - * API doesn't name that header in `Access-Control-Expose-Headers` — so a - * browser withholds it from JS and every page looks like the last one. - * Paginating with a `limit` can't work there until the API exposes the header; - * the SDK can't reach around CORS. - */ -export const canReadPaginationToken = runtime !== 'browser' - /** * Whether the test can observe a sandbox that has stopped. * From ae300e77f842c13330d706fd2b7822b992fe41c7 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:33:36 +0200 Subject: [PATCH 3/9] test(js-sdk): unskip browser tests fixed by infra#3389 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes canObserveStoppedSandbox. The stopped-sandbox 502 arriving from the edge without CORS headers is fixed in e2b-dev/infra#3389, so isRunning() can report false in a browser again and checkSandboxHealth can return false, which restores the actionable TimeoutError on the 24 call sites that use it. Also un-gates host.test.ts 'ping server in non-running sandbox', which was under the wrong flag: it asserts the edge's own 502 JSON envelope rather than a response from a server inside the sandbox, so infra#3389 covers it too (the same PR's content-negotiation fix is what keeps the body JSON rather than the HTML error page). Its sibling 'ping server in running sandbox' does read the user's python server, so that one keeps the gate. canFetchSandboxServers stays for the four tests that read a response from a server the test started in the sandbox — no CORS headers there, and one of them sends a custom header that would preflight into python's 501. Its doc comment no longer cites the proxy 502 as an example, since that now carries the header. Until infra#3389 reaches production these 14 tests fail on the browser leg — 13 with 'TypeError: Failed to fetch' and one with the degraded SandboxError in place of TimeoutError, matching the two documented consequences exactly. The Node/Bun/Deno/workerd legs are unaffected (7 files / 26 tests green on Node). Co-Authored-By: Claude --- packages/js-sdk/tests/api/snapshot.test.ts | 46 ++++++++----------- .../commands/sandboxKilledDuringRun.test.ts | 4 +- packages/js-sdk/tests/sandbox/connect.test.ts | 9 +--- packages/js-sdk/tests/sandbox/host.test.ts | 2 +- .../tests/sandbox/lifecyclePayload.test.ts | 10 +--- .../js-sdk/tests/sandbox/snapshot.test.ts | 16 +++---- packages/js-sdk/tests/sandbox/timeout.test.ts | 22 +++------ packages/js-sdk/tests/setup.ts | 22 +++------ 8 files changed, 48 insertions(+), 83 deletions(-) diff --git a/packages/js-sdk/tests/api/snapshot.test.ts b/packages/js-sdk/tests/api/snapshot.test.ts index cf66a9a872..828389d143 100644 --- a/packages/js-sdk/tests/api/snapshot.test.ts +++ b/packages/js-sdk/tests/api/snapshot.test.ts @@ -1,32 +1,26 @@ import { assert } from 'vitest' -import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../setup.js' +import { sandboxTest, isDebug } from '../setup.js' import { Sandbox } from '../../src' -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( - 'pause sandbox', - async ({ sandbox }) => { - await Sandbox.pause(sandbox.sandboxId) - assert.isFalse( - await sandbox.isRunning(), - 'Sandbox should not be running after pause' - ) - } -) +sandboxTest.skipIf(isDebug)('pause sandbox', async ({ sandbox }) => { + await Sandbox.pause(sandbox.sandboxId) + assert.isFalse( + await sandbox.isRunning(), + 'Sandbox should not be running after pause' + ) +}) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( - 'resume sandbox', - async ({ sandbox }) => { - await Sandbox.pause(sandbox.sandboxId) - assert.isFalse( - await sandbox.isRunning(), - 'Sandbox should not be running after pause' - ) +sandboxTest.skipIf(isDebug)('resume sandbox', async ({ sandbox }) => { + await Sandbox.pause(sandbox.sandboxId) + assert.isFalse( + await sandbox.isRunning(), + 'Sandbox should not be running after pause' + ) - await Sandbox.connect(sandbox.sandboxId) - assert.isTrue( - await sandbox.isRunning(), - 'Sandbox should be running after resume' - ) - } -) + await Sandbox.connect(sandbox.sandboxId) + assert.isTrue( + await sandbox.isRunning(), + 'Sandbox should be running after resume' + ) +}) diff --git a/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts b/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts index 80e170db7b..511a7a38a2 100644 --- a/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts +++ b/packages/js-sdk/tests/sandbox/commands/sandboxKilledDuringRun.test.ts @@ -1,9 +1,9 @@ import { expect } from 'vitest' import { TimeoutError } from '../../../src/index.js' -import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../../setup.js' +import { sandboxTest, isDebug } from '../../setup.js' -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'killing the sandbox while a command is running throws an actionable error', async ({ sandbox }) => { const cmd = await sandbox.commands.run('sleep 60', { background: true }) diff --git a/packages/js-sdk/tests/sandbox/connect.test.ts b/packages/js-sdk/tests/sandbox/connect.test.ts index 02f221b64f..9b7ca5f5f6 100644 --- a/packages/js-sdk/tests/sandbox/connect.test.ts +++ b/packages/js-sdk/tests/sandbox/connect.test.ts @@ -1,12 +1,7 @@ import { assert, test, expect, vi } from 'vitest' import { Sandbox } from '../../src' -import { - isDebug, - sandboxTest, - template, - canObserveStoppedSandbox, -} from '../setup.js' +import { isDebug, sandboxTest, template } from '../setup.js' test('connect in debug mode does not call the API', async () => { const fetchSpy = vi.fn(() => { @@ -47,7 +42,7 @@ test.skipIf(isDebug)('connect', async () => { } }) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'connect resumes paused sandbox', async ({ sandbox }) => { await sandbox.pause() diff --git a/packages/js-sdk/tests/sandbox/host.test.ts b/packages/js-sdk/tests/sandbox/host.test.ts index 47045b7e8c..3f36f1b09d 100644 --- a/packages/js-sdk/tests/sandbox/host.test.ts +++ b/packages/js-sdk/tests/sandbox/host.test.ts @@ -39,7 +39,7 @@ sandboxTest.skipIf(!canFetchSandboxServers)( 60_000 ) -sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( +sandboxTest.skipIf(isDebug)( 'ping server in non-running sandbox', async ({ sandbox }) => { const host = sandbox.getHost(3000) diff --git a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts index 8eee24560b..a4ee886f68 100644 --- a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts @@ -1,13 +1,7 @@ import { assert, expect, test } from 'vitest' import { InvalidArgumentError, Sandbox } from '../../src' -import { - isDebug, - template, - wait, - canObserveStoppedSandbox, - canFetchSandboxServers, -} from '../setup.js' +import { isDebug, template, wait, canFetchSandboxServers } from '../setup.js' test.skipIf(isDebug)( 'filesystem-only auto-pause cannot be combined with auto-resume', @@ -44,7 +38,7 @@ test.skipIf(isDebug)( } ) -test.skipIf(isDebug || !canObserveStoppedSandbox)( +test.skipIf(isDebug)( 'auto-pause without auto-resume requires connect to wake', async () => { const sandbox = await Sandbox.create(template, { diff --git a/packages/js-sdk/tests/sandbox/snapshot.test.ts b/packages/js-sdk/tests/sandbox/snapshot.test.ts index 8b55bc47e8..ac0dead40d 100644 --- a/packages/js-sdk/tests/sandbox/snapshot.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot.test.ts @@ -1,8 +1,8 @@ import { assert, describe } from 'vitest' -import { sandboxTest, isDebug, canObserveStoppedSandbox } from '../setup.js' +import { sandboxTest, isDebug } from '../setup.js' -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox', async ({ sandbox }) => { assert.isTrue(await sandbox.isRunning()) @@ -25,7 +25,7 @@ describe('pause and resume with env vars', () => { }, }) - sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( + sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with env vars', async ({ sandbox }) => { // Environment variables of a process exist at runtime, and are not stored in some file or so. @@ -52,7 +52,7 @@ describe('pause and resume with env vars', () => { ) }) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with file', async ({ sandbox }) => { const filename = 'test_snapshot.txt' @@ -81,7 +81,7 @@ sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( } ) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with ongoing long running process', async ({ sandbox }) => { const cmd = await sandbox.commands.run('sleep 3600', { background: true }) @@ -105,7 +105,7 @@ sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( } ) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with completed long running process', async ({ sandbox }) => { const filename = 'test_long_running.txt' @@ -137,7 +137,7 @@ sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( } ) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with http server', async ({ sandbox }) => { await sandbox.commands.run('python3 -m http.server 8000', { @@ -163,7 +163,7 @@ sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( } ) -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( +sandboxTest.skipIf(isDebug)( 'filesystem-only pause reboots on resume but keeps the filesystem', async ({ sandbox }) => { // Absolute path: a cold boot may not restore the template's default diff --git a/packages/js-sdk/tests/sandbox/timeout.test.ts b/packages/js-sdk/tests/sandbox/timeout.test.ts index 29881c5fc9..249666ada3 100644 --- a/packages/js-sdk/tests/sandbox/timeout.test.ts +++ b/packages/js-sdk/tests/sandbox/timeout.test.ts @@ -1,22 +1,14 @@ import { expect } from 'vitest' -import { - sandboxTest, - isDebug, - wait, - canObserveStoppedSandbox, -} from '../setup.js' - -sandboxTest.skipIf(isDebug || !canObserveStoppedSandbox)( - 'shorten timeout', - async ({ sandbox }) => { - await sandbox.setTimeout(5000) +import { sandboxTest, isDebug, wait } from '../setup.js' - await wait(6000) +sandboxTest.skipIf(isDebug)('shorten timeout', async ({ sandbox }) => { + await sandbox.setTimeout(5000) - expect(await sandbox.isRunning()).toBeFalsy() - } -) + await wait(6000) + + expect(await sandbox.isRunning()).toBeFalsy() +}) sandboxTest.skipIf(isDebug)( 'shorten then lengthen timeout', diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index a10484a058..203db66bb4 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -153,25 +153,15 @@ export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` * Whether the test can read a response from a server running inside a sandbox. * * A browser only exposes a cross-origin response to JS when the server opts in - * with CORS headers. Servers a test starts in a sandbox (`python -m - * http.server`, and the sandbox proxy's own 502 page) don't, so there the fetch - * fails as an opaque `TypeError: Failed to fetch` no matter what the server - * answered. Nothing the SDK can paper over — a real browser app would hit the - * same wall. + * with CORS headers. A plain server a test starts in a sandbox (`python -m + * http.server`) doesn't, so there the fetch fails as an opaque `TypeError: + * Failed to fetch` no matter what the server answered. Nothing the SDK can + * paper over — a real browser app would hit the same wall. This is about the + * user's own server only: errors the sandbox proxy synthesizes do carry CORS + * headers (infra#3389). */ export const canFetchSandboxServers = runtime !== 'browser' -/** - * Whether the test can observe a sandbox that has stopped. - * - * `isRunning()` reads envd's `/health` over the sandbox host, and a stopped - * sandbox answers 502 from the edge without CORS headers — so in a browser the - * probe rejects instead of reporting `false`. That also costs the health-check - * refinement in `handleRpcErrorWithHealthCheck`: a connection dropped by a kill - * stays a generic `SandboxError` there instead of a `TimeoutError`. - */ -export const canObserveStoppedSandbox = runtime !== 'browser' - function generateRandomString(length: number = 8): string { return Math.random() .toString(36) From a80a8ec1692854420d411bf1898ca9014db2a816 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:14:07 +0200 Subject: [PATCH 4/9] test(js-sdk): address browser-leg review feedback - gate the http-server pause/resume test on canFetchSandboxServers: it fetches a `python -m http.server` the test starts itself, which sends no CORS headers. Masked today by the isRunning failure, it would have started failing the moment infra#3389 rolls out. - import the SDK dynamically in noProcessGlobal, after the process shim is deleted. A static import is evaluated at collection time while the shim is still installed, so a top-level `process` read anywhere in the module graph would have passed here and still crashed a browser app on import. Co-Authored-By: Claude --- .../js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts | 7 ++++++- packages/js-sdk/tests/sandbox/snapshot.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts index e8ec18c2f3..7c5237c3e6 100644 --- a/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts +++ b/packages/js-sdk/tests/runtimes/browser/noProcessGlobal.test.ts @@ -1,6 +1,5 @@ import { assert, test } from 'vitest' -import { Sandbox } from '../../../src' import { template } from '../../template' // Bundlers don't give browser apps a `process` unless asked to, so the SDK has @@ -11,6 +10,11 @@ import { template } from '../../template' // `process` read on the create path, so this test drops the shim entirely and // drives a real sandbox the way a browser app has to. // +// The SDK is imported dynamically inside the test, after the shim is gone: a +// static import is evaluated at collection time, while the shim is still +// installed, which would let a top-level `process` read anywhere in the module +// graph pass here and still crash a browser app on import. +// // Config comes from `import.meta.env` (where vitest puts the config's `env`), // which is also how a Vite app would hand its own build-time values over. const apiKey = import.meta.env.E2B_API_KEY @@ -25,6 +29,7 @@ test.skipIf(isDebug)( assert.equal(typeof process, 'undefined') try { + const { Sandbox } = await import('../../../src') const sandbox = await Sandbox.create(template, { apiKey, domain }) try { diff --git a/packages/js-sdk/tests/sandbox/snapshot.test.ts b/packages/js-sdk/tests/sandbox/snapshot.test.ts index ac0dead40d..f276432061 100644 --- a/packages/js-sdk/tests/sandbox/snapshot.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot.test.ts @@ -1,6 +1,6 @@ import { assert, describe } from 'vitest' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, canFetchSandboxServers } from '../setup.js' sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox', @@ -137,7 +137,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( 'pause and resume a sandbox with http server', async ({ sandbox }) => { await sandbox.commands.run('python3 -m http.server 8000', { From 6fd735e0b3892d729639ad855bb5dcf2ffcf1b6c Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:25:57 +0200 Subject: [PATCH 5/9] test(js-sdk): serve CORS from in-sandbox test servers, drop the last browser gate `python -m http.server` sends no CORS headers, so a browser handed the test an opaque `TypeError: Failed to fetch` instead of the response. That was gated behind `canFetchSandboxServers` as inherent, but it isn't: the server is ours, and a real browser app's own server opts in the same way. `corsHttpServerCmd` starts one that does, so the five tests run in Chromium too and the last capability flag is gone. Verified in Chromium against a local instance of the exact generated command: cross-origin GET readable, and a custom-header request survives the preflight via do_OPTIONS. Negative control against a plain `python -m http.server` reproduces the `Failed to fetch` the browser leg was showing. Co-Authored-By: Claude --- .../tests/runtimes/browser/vitest.config.mts | 8 ++-- packages/js-sdk/tests/sandbox/host.test.ts | 6 +-- .../tests/sandbox/lifecyclePayload.test.ts | 6 +-- packages/js-sdk/tests/sandbox/network.test.ts | 10 ++--- .../js-sdk/tests/sandbox/snapshot.test.ts | 6 +-- packages/js-sdk/tests/setup.ts | 38 ++++++++++++++----- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts index f816c190d8..7d70d0cab5 100644 --- a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts +++ b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts @@ -17,9 +17,11 @@ const testEnv = Object.fromEntries( // Runs the unit + connectionConfig projects (same coverage as test:bun / // test:deno / test:cf) inside a real Chromium via Playwright, against src. -// Tests the browser physically can't run are skipped from the test files -// themselves — see the capability flags in tests/setup.ts — so they stay -// visible in the report instead of disappearing into the exclude list below. +// Nothing is skipped for being a browser: the suites that can't run here are +// Node-only rather than browser-hostile, and they're excluded below. Tests that +// read a response from a server inside the sandbox start a CORS-enabled one +// (`corsHttpServerCmd` in tests/setup.ts), the way a browser app's own server +// would be configured. export default defineConfig({ test: { name: 'browser', diff --git a/packages/js-sdk/tests/sandbox/host.test.ts b/packages/js-sdk/tests/sandbox/host.test.ts index 3f36f1b09d..a6712536a1 100644 --- a/packages/js-sdk/tests/sandbox/host.test.ts +++ b/packages/js-sdk/tests/sandbox/host.test.ts @@ -1,11 +1,11 @@ import { assert } from 'vitest' -import { isDebug, sandboxTest, wait, canFetchSandboxServers } from '../setup.js' +import { isDebug, sandboxTest, wait, corsHttpServerCmd } from '../setup.js' import { catchCmdExitErrorInBackground } from '../cmdHelper.js' -sandboxTest.skipIf(!canFetchSandboxServers)( +sandboxTest( 'ping server in running sandbox', async ({ sandbox }) => { - const cmd = await sandbox.commands.run('python -m http.server 8000', { + const cmd = await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts index a4ee886f68..c51c6344ce 100644 --- a/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts +++ b/packages/js-sdk/tests/sandbox/lifecyclePayload.test.ts @@ -1,7 +1,7 @@ import { assert, expect, test } from 'vitest' import { InvalidArgumentError, Sandbox } from '../../src' -import { isDebug, template, wait, canFetchSandboxServers } from '../setup.js' +import { isDebug, template, wait, corsHttpServerCmd } from '../setup.js' test.skipIf(isDebug)( 'filesystem-only auto-pause cannot be combined with auto-resume', @@ -111,7 +111,7 @@ test.skipIf(isDebug)( 60_000 ) -test.skipIf(isDebug || !canFetchSandboxServers)( +test.skipIf(isDebug)( 'auto-resume wakes paused sandbox on http request', async () => { const sandbox = await Sandbox.create(template, { @@ -123,7 +123,7 @@ test.skipIf(isDebug || !canFetchSandboxServers)( }) try { - await sandbox.commands.run('python3 -m http.server 8000', { + await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/network.test.ts b/packages/js-sdk/tests/sandbox/network.test.ts index 273e4dec3a..2bdaf16247 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -5,7 +5,7 @@ import { sandboxTest, isDebug, template, - canFetchSandboxServers, + corsHttpServerCmd, } from '../setup.js' import { httpbinTemplate } from '../template.js' @@ -135,7 +135,7 @@ describe('allowPublicTraffic=false', () => { }, }) - sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( + sandboxTest.skipIf(isDebug)( 'sandbox requires traffic access token', async ({ sandbox }) => { // Verify the sandbox was created successfully and has a traffic access token @@ -143,7 +143,7 @@ describe('allowPublicTraffic=false', () => { // Start a simple HTTP server in the sandbox const port = 8080 - sandbox.commands.run(`python3 -m http.server ${port}`, { + sandbox.commands.run(corsHttpServerCmd(port), { background: true, }) @@ -177,12 +177,12 @@ describe('allowPublicTraffic=true', () => { }, }) - sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( + sandboxTest.skipIf(isDebug)( 'sandbox works without token', async ({ sandbox }) => { // Start a simple HTTP server in the sandbox const port = 8080 - sandbox.commands.run(`python3 -m http.server ${port}`, { + sandbox.commands.run(corsHttpServerCmd(port), { background: true, }) diff --git a/packages/js-sdk/tests/sandbox/snapshot.test.ts b/packages/js-sdk/tests/sandbox/snapshot.test.ts index f276432061..47a85cfea1 100644 --- a/packages/js-sdk/tests/sandbox/snapshot.test.ts +++ b/packages/js-sdk/tests/sandbox/snapshot.test.ts @@ -1,6 +1,6 @@ import { assert, describe } from 'vitest' -import { sandboxTest, isDebug, canFetchSandboxServers } from '../setup.js' +import { sandboxTest, isDebug, corsHttpServerCmd } from '../setup.js' sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox', @@ -137,10 +137,10 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug || !canFetchSandboxServers)( +sandboxTest.skipIf(isDebug)( 'pause and resume a sandbox with http server', async ({ sandbox }) => { - await sandbox.commands.run('python3 -m http.server 8000', { + await sandbox.commands.run(corsHttpServerCmd(8000), { background: true, }) diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 203db66bb4..97e98b4b78 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -8,7 +8,6 @@ import { TemplateClass, Volume, } from '../src' -import { runtime } from '../src/utils' import { template } from './template' interface SandboxFixture { @@ -150,17 +149,36 @@ export const isDebug = process.env.E2B_DEBUG !== undefined export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` /** - * Whether the test can read a response from a server running inside a sandbox. + * Command that serves the working directory on `port` with CORS enabled, for + * tests that read a response from a server running inside the sandbox. * - * A browser only exposes a cross-origin response to JS when the server opts in - * with CORS headers. A plain server a test starts in a sandbox (`python -m - * http.server`) doesn't, so there the fetch fails as an opaque `TypeError: - * Failed to fetch` no matter what the server answered. Nothing the SDK can - * paper over — a real browser app would hit the same wall. This is about the - * user's own server only: errors the sandbox proxy synthesizes do carry CORS - * headers (infra#3389). + * Use this instead of `python -m http.server`, which sends no CORS headers: a + * browser only exposes a cross-origin response to JS when the server opts in, + * so there the fetch fails as an opaque `TypeError: Failed to fetch` no matter + * what the server answered. Opting in is what a real browser app's own server + * does too, and it's only ever the user's server that has to — everything the + * sandbox proxy answers itself already carries CORS headers (infra#3389). + * + * `Access-Control-Allow-Headers` and `do_OPTIONS` cover requests that carry a + * custom header, which the browser preflights before sending. */ -export const canFetchSandboxServers = runtime !== 'browser' +export function corsHttpServerCmd(port: number): string { + return `python3 -c " +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer + +class Handler(SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Headers', '*') + SimpleHTTPRequestHandler.end_headers(self) + + def do_OPTIONS(self): + self.send_response(204) + self.end_headers() + +ThreadingHTTPServer(('', ${port}), Handler).serve_forever() +"` +} function generateRandomString(length: number = 8): string { return Math.random() From 785b93871d5411a8c29db6735969ad623b4d32ef Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:57:28 +0200 Subject: [PATCH 6/9] test(js-sdk): exclude the msw suites main added since the browser leg was written MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six new suites import msw's `setupServer`, whose `msw/node` entry pulls in node:http and can't be served to a browser. Also drops ENABLE_VOLUME_TESTS from the forwarded env — main removed that flag when it mocked the volume tests. Co-Authored-By: Claude --- .../tests/runtimes/browser/vitest.config.mts | 20 ++++++++++++------- packages/js-sdk/tests/sandbox/network.test.ts | 7 +------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts index 7d70d0cab5..b27276bdaf 100644 --- a/packages/js-sdk/tests/runtimes/browser/vitest.config.mts +++ b/packages/js-sdk/tests/runtimes/browser/vitest.config.mts @@ -5,14 +5,11 @@ import { defineConfig } from 'vitest/config' // Real env vars win over `.env`, matching dotenv's own precedence. const env = { ...config().parsed, ...process.env } -// Config the shared suites read from the environment. Everything the SDK takes -// is `E2B_`-prefixed; ENABLE_VOLUME_TESTS gates the volume suite. Forwarded by -// name rather than as the whole environment, which would inline every host -// variable into JS served to the browser. +// Config the shared suites read from the environment; everything the SDK takes +// is `E2B_`-prefixed. Forwarded by name rather than as the whole environment, +// which would inline every host variable into JS served to the browser. const testEnv = Object.fromEntries( - Object.entries(env).filter( - ([name]) => name.startsWith('E2B_') || name === 'ENABLE_VOLUME_TESTS' - ) + Object.entries(env).filter(([name]) => name.startsWith('E2B_')) ) as Record // Runs the unit + connectionConfig projects (same coverage as test:bun / @@ -46,7 +43,16 @@ export default defineConfig({ // These mock the API with msw's `setupServer`, whose `msw/node` entry // pulls in node:http and can't be served to the browser. Porting them // means `setupWorker` plus a service worker served from a public dir. + // Any new suite that imports `msw/node` belongs here — `grep -rl msw/node + // tests/` lists the full set (tests/template/** is already excluded). + 'tests/client.test.ts', 'tests/sandbox/abortSignal.test.ts', + 'tests/sandbox/egressProxy.test.ts', + 'tests/sandbox/iam.test.ts', + 'tests/sandbox/lifecycleRequest.test.ts', + 'tests/sandbox/networkTransform.test.ts', + 'tests/secret/secret.test.ts', + 'tests/volume/file.test.ts', 'tests/volume/volume.test.ts', ], globals: false, diff --git a/packages/js-sdk/tests/sandbox/network.test.ts b/packages/js-sdk/tests/sandbox/network.test.ts index 2bdaf16247..08fcc12d5d 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -1,12 +1,7 @@ import { assert, expect, describe } from 'vitest' import { CommandExitError, Sandbox } from '../../src' -import { - sandboxTest, - isDebug, - template, - corsHttpServerCmd, -} from '../setup.js' +import { sandboxTest, isDebug, template, corsHttpServerCmd } from '../setup.js' import { httpbinTemplate } from '../template.js' describe('allow only 1.1.1.1', () => { From 33c28e8136fbdd3c909c707d5c8f964b6592360a Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:02:07 +0200 Subject: [PATCH 7/9] fix(js-sdk): drain streams through the reader when buffering an upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buffering a ReadableStream went through `new Response(stream)`, whose accepted body types differ per runtime: undici takes any async iterable, a browser takes only its own stream class and stringifies the rest. So a browser uploading a stream from a polyfill or another realm silently sent the 23-byte string "[object ReadableStream]" instead of the file. Draining through the reader — the one part every implementation shares — is correct everywhere and drops the runtime-dependent guess. Note the obvious fix (skip the async-iterable branch of `toDispatchableStream` in the browser) is wrong: it re-wraps a native stream when `globalThis.ReadableStream` has been replaced by a polyfill, which is what that branch exists to prevent. Found by the browser test leg via `toUploadBody leaves an async-iterable foreign stream alone`, which main added while this branch was in review. Co-Authored-By: Claude --- .changeset/olive-jars-tap.md | 5 +++++ packages/js-sdk/src/utils.ts | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .changeset/olive-jars-tap.md diff --git a/.changeset/olive-jars-tap.md b/.changeset/olive-jars-tap.md new file mode 100644 index 0000000000..61108f9ba0 --- /dev/null +++ b/.changeset/olive-jars-tap.md @@ -0,0 +1,5 @@ +--- +'e2b': patch +--- + +Fix uploads of non-native `ReadableStream`s in the browser silently sending the text `[object ReadableStream]` instead of the data. Buffering a stream drained it with `new Response(stream)`, which accepts any async iterable on Node (an undici extension) but only its own stream class in a browser, stringifying anything else. Streams are now drained through the reader, which every implementation supports. diff --git a/packages/js-sdk/src/utils.ts b/packages/js-sdk/src/utils.ts index 9db66d7bf1..a0c598d3a7 100644 --- a/packages/js-sdk/src/utils.ts +++ b/packages/js-sdk/src/utils.ts @@ -168,9 +168,22 @@ export async function toBlob( if (isBlobLike(data)) { return new Blob([await data.arrayBuffer()], { type: data.type }) } - // ReadableStream - must consume to get Blob + // ReadableStream - must consume to get Blob. Drained through the reader + // rather than `new Response(stream)`: the platform only accepts a body it + // recognizes, and what counts differs per runtime (undici takes any async + // iterable, a browser takes only its own stream class and stringifies the + // rest to "[object ReadableStream]"). The reader is the portable part. if (isReadableStreamLike(data)) { - return new Response(toDispatchableStream(data)).blob() + const reader = data.getReader() + const chunks: BlobPart[] = [] + for (;;) { + const { done, value } = await reader.read() + if (done) { + break + } + chunks.push(value) + } + return new Blob(chunks) } // String or ArrayBuffer - create Blob. A cross-realm ArrayBuffer needs no // special handling: buffer sources are recognized by V8, not by brand. From d04fb048b7b12ff144424ce44c2109c871e9523c Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:58:23 +0200 Subject: [PATCH 8/9] fix(js-sdk): recognize the browser's mid-request connection drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a sandbox is killed while a request is in flight the SDK probes the sandbox's health to tell that apart from a transient network failure, and returns an actionable TimeoutError when the probe confirms the sandbox is gone. The probe is gated on the connection-dropped message, whose wording is runtime-specific — and the browser's, `network error`, was missing, so killing a sandbox mid-command surfaced a generic `SandboxError: [unknown] network error`. The fragment is broad, which is fine by construction: matching only earns a health probe, and the error stays generic unless the probe confirms the sandbox is gone. A browser reports every failed request identically on purpose, so this is the only wording available. Found by the browser test leg, which attributed this to the missing CORS headers on proxy-synthesized responses. It wasn't that: belt#2068 rolled out to production and the other 13 failures went green, this one didn't. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/soft-moons-repeat.md | 5 +++++ packages/js-sdk/src/envd/rpc.ts | 2 ++ packages/js-sdk/tests/envd/handleEnvdApiError.test.ts | 1 + packages/js-sdk/tests/envd/handleRpcError.test.ts | 1 + 4 files changed, 9 insertions(+) create mode 100644 .changeset/soft-moons-repeat.md diff --git a/.changeset/soft-moons-repeat.md b/.changeset/soft-moons-repeat.md new file mode 100644 index 0000000000..ab6668326e --- /dev/null +++ b/.changeset/soft-moons-repeat.md @@ -0,0 +1,5 @@ +--- +'e2b': patch +--- + +Report a sandbox killed mid-request as an actionable `TimeoutError` in the browser. When the connection to a sandbox drops mid-request the SDK probes the sandbox's health to tell a killed sandbox apart from a transient network blip, but the probe only ran for connection-dropped wordings it recognized, and the browser's (`network error`) was missing — so killing a sandbox while a command was running surfaced a generic `SandboxError: [unknown] network error` instead of `TimeoutError: ... The sandbox was killed or reached its end of life while the request was in flight.` diff --git a/packages/js-sdk/src/envd/rpc.ts b/packages/js-sdk/src/envd/rpc.ts index 9c5b4ad1ba..5e03225742 100644 --- a/packages/js-sdk/src/envd/rpc.ts +++ b/packages/js-sdk/src/envd/rpc.ts @@ -29,12 +29,14 @@ export type SandboxHealthCheck = () => Promise * - Bun: `The socket connection was closed unexpectedly` * - Deno: `error reading a body from connection` * - Cloudflare Workers: `Network connection lost` + * - Browser: `network error` */ const CONNECTION_TERMINATED_MESSAGES = [ 'terminated', 'The socket connection was closed unexpectedly', 'error reading a body from connection', 'Network connection lost', + 'network error', ] /** diff --git a/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts b/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts index b886f3fe51..4140741946 100644 --- a/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts +++ b/packages/js-sdk/tests/envd/handleEnvdApiError.test.ts @@ -126,6 +126,7 @@ describe('handleEnvdApiFetchError', () => { Bun: new Error('The socket connection was closed unexpectedly'), Deno: new TypeError('error reading a body from connection'), 'Cloudflare Workers': new Error('Network connection lost.'), + Browser: new TypeError('network error'), } for (const [runtime, error] of Object.entries(runtimeTerminatedErrors)) { diff --git a/packages/js-sdk/tests/envd/handleRpcError.test.ts b/packages/js-sdk/tests/envd/handleRpcError.test.ts index 8df1a6d6a6..f90f893e64 100644 --- a/packages/js-sdk/tests/envd/handleRpcError.test.ts +++ b/packages/js-sdk/tests/envd/handleRpcError.test.ts @@ -70,6 +70,7 @@ describe('handleRpcErrorWithHealthCheck', () => { Bun: 'The socket connection was closed unexpectedly', Deno: 'error reading a body from connection', 'Cloudflare Workers': 'Network connection lost.', + Browser: 'network error', } test('returns a TimeoutError when the health check says the sandbox is not running', async () => { From 3288bcdf3372aa873a666dfca36d6f81d9939b51 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:36:32 +0200 Subject: [PATCH 9/9] test(js-sdk): point the CORS note at belt#2068 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit infra#3389 was closed unmerged; the fix that landed is belt#2068. Note also which paths are still awaiting the orchestrator rollout, since that is what keeps `sandbox requires traffic access token` red on the browser leg. Comment only — the test stays un-gated deliberately, so the leg goes green on its own once the rollout completes. Co-Authored-By: Claude Opus 5 (1M context) --- packages/js-sdk/tests/setup.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/js-sdk/tests/setup.ts b/packages/js-sdk/tests/setup.ts index 97e98b4b78..fc977c0f86 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -156,8 +156,10 @@ export const TEST_API_KEY = `e2b_${'0'.repeat(40)}` * browser only exposes a cross-origin response to JS when the server opts in, * so there the fetch fails as an opaque `TypeError: Failed to fetch` no matter * what the server answered. Opting in is what a real browser app's own server - * does too, and it's only ever the user's server that has to — everything the - * sandbox proxy answers itself already carries CORS headers (infra#3389). + * does too, and it's only ever the user's server that has to — the responses + * the sandbox proxy synthesizes carry their own CORS headers (belt#2068; the + * traffic-token paths are still awaiting the orchestrator rollout, which is why + * `sandbox requires traffic access token` is red on the browser leg). * * `Access-Control-Allow-Headers` and `do_OPTIONS` cover requests that carry a * custom header, which the browser preflights before sending.