From 5626cfff60bcb7e68d7e24bfa4c4a5c862cf8824 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/5] 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 | 31 +- packages/js-sdk/.gitignore | 2 + packages/js-sdk/package.json | 10 +- 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 | 6 +- .../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 | 550 +----------------- 35 files changed, 348 insertions(+), 790 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 d5725246c0..c374eaa853 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 }} steps: - name: Checkout repository @@ -72,18 +75,17 @@ 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@v4 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 }} # The unit bundle test and the Cloudflare deploy config fail in CI when @@ -139,3 +141,12 @@ jobs: env: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + + # Full unit + connectionConfig suite inside Chromium (vitest browser mode); + # `pretest:browser` installs the browser and its system dependencies. + - 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 b11d1b94ff..fc51fd9360 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -35,7 +35,8 @@ "generate:volume-api": "openapi-typescript ../../spec/openapi-volumecontent.yml -x api_key --array-length --alphabetize --output src/volume/schema.gen.ts", "generate:mcp": "json2ts -i ./../../spec/mcp-server.json -o src/sandbox/mcp.d.ts --unreachableDefinitions --style.singleQuote --no-style.semi", "check-deps": "knip", - "pretest": "npx playwright install --with-deps chromium", + "pretest:browser": "npx playwright install --with-deps chromium", + "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", @@ -47,13 +48,9 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.18.7", "@redocly/cli": "2.39.0", - "@testing-library/react": "^16.2.0", "@types/node": "^20.19.19", "@types/platform": "^1.3.6", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", "@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", @@ -64,12 +61,9 @@ "npm-run-all": "^4.1.5", "openapi-typescript": "^7.9.1", "playwright": "^1.55.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", "tsdown": "^0.22.3", "typescript": "npm:@typescript/typescript6@^6.0.2", "vitest": "^4.1.10", - "vitest-browser-react": "^2.2.0", "wrangler": "^4.113.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 3960b9fffd..154e4cc2fd 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 @@ -237,7 +236,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 { @@ -303,7 +302,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -339,7 +338,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug)( +sandboxTest.skipIf(isDebug || !canReadPaginationToken)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await Sandbox.betaPause(sandbox.sandboxId) @@ -378,7 +377,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 1af6edc8c4..843c6b1cc7 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 } @@ -61,21 +62,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 ae8da00415..6e1e245e5c 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -1,7 +1,7 @@ import { assert, expect, describe } from 'vitest' import { CommandExitError } from '../../src' -import { sandboxTest, isDebug } from '../setup.js' +import { sandboxTest, isDebug, canFetchSandboxServers } from '../setup.js' describe('allow only 1.1.1.1', () => { sandboxTest.override({ @@ -129,7 +129,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 @@ -171,7 +171,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 e29c20eba5..509c41c0dc 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 { @@ -144,6 +145,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 48a23cffe2..d21962d511 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,27 +177,15 @@ importers: '@redocly/cli': specifier: 2.39.0 version: 2.39.0 - '@testing-library/react': - specifier: ^16.2.0 - version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@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.0 - version: 19.2.17 - '@types/react-dom': - specifier: ^19.2.0 - version: 19.2.3(@types/react@19.2.17) '@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.12.14(@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) @@ -228,12 +216,6 @@ importers: playwright: specifier: ^1.55.1 version: 1.55.1 - react: - specifier: ^19.2.0 - version: 19.2.7 - react-dom: - specifier: ^19.2.0 - version: 19.2.7(react@19.2.7) tsdown: specifier: ^0.22.3 version: 0.22.4(@typescript/typescript6@6.0.2) @@ -243,9 +225,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.12.14(@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.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.10) wrangler: specifier: ^4.113.0 version: 4.113.0(@types/node@20.19.43)(bufferutil@4.0.8)(utf-8-validate@6.0.3) @@ -262,60 +241,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.7': - 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'} @@ -324,65 +253,15 @@ packages: 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.27.1': resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} engines: {node: '>=6.9.0'} - '@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'} @@ -1098,9 +977,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'} @@ -1565,43 +1441,9 @@ 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.2.0': - resolution: {integrity: sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==} - 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 - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@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==} @@ -1639,14 +1481,6 @@ packages: '@types/platform@1.3.6': resolution: {integrity: sha512-ZmSaqHuvzv+jC232cFoz2QqPUkaj6EvMmCrWcx3WRr7xTPVFCMUOTcOq8m2d+Zw1iKRc1kDiaA+jtNrV0hkVew==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/statuses@2.0.5': resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} @@ -1780,12 +1614,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: @@ -1984,10 +1812,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'} @@ -2005,9 +1829,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'} @@ -2067,11 +1888,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==} @@ -2102,9 +1918,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'} @@ -2216,9 +2029,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'} @@ -2258,10 +2068,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-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2273,9 +2079,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'} @@ -2303,9 +2106,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==} @@ -2466,10 +2266,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.*} @@ -2824,11 +2620,6 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} 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==} @@ -2847,11 +2638,6 @@ packages: json2md@2.0.1: resolution: {integrity: sha512-VbwmZ83qmUfKBS2pUOHlzNKEZFPBeJSbzEok3trMYyboZUgdHNn1XZfc1uT8UZs1GHCrmRUBXCfqw4YmmQuOhw==} - 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==} @@ -2885,13 +2671,6 @@ packages: resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} 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==} @@ -2992,9 +2771,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==} @@ -3225,10 +3001,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'} @@ -3258,22 +3030,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - 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.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - read-pkg@3.0.0: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} @@ -3373,17 +3129,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'} @@ -3639,10 +3388,6 @@ packages: resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} 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'} @@ -3776,12 +3521,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==} @@ -3836,20 +3575,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} @@ -3985,9 +3710,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'} @@ -4059,144 +3781,18 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@7.29.7': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - 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.27.1': {} - '@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 @@ -4717,11 +4313,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': {} @@ -5083,55 +4675,11 @@ 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.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@babel/runtime': 7.27.1 - '@testing-library/dom': 10.4.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true - '@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 @@ -5166,14 +4714,6 @@ snapshots: '@types/platform@1.3.6': {} - '@types/react-dom@19.2.3(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - '@types/statuses@2.0.5': {} '@types/statuses@2.0.6': {} @@ -5246,17 +4786,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.12.14(@types/node@20.19.43)(@typescript/typescript6@6.0.2))(playwright@1.55.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.12.14(@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) @@ -5441,8 +4970,6 @@ snapshots: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.1: {} ansis@4.3.1: {} @@ -5455,10 +4982,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 @@ -5528,13 +5051,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 @@ -5568,8 +5084,6 @@ snapshots: camelcase@7.0.1: {} - caniuse-lite@1.0.30001753: {} - chai@6.2.2: {} chalk@2.4.2: @@ -5675,8 +5189,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 @@ -5726,8 +5238,6 @@ snapshots: defu@6.1.7: {} - dequal@2.0.3: {} - detect-libc@2.1.2: {} dir-glob@3.0.1: @@ -5739,8 +5249,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: {} @@ -5775,8 +5283,6 @@ snapshots: optionalDependencies: wcwidth: 1.0.1 - electron-to-chromium@1.5.80: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -6035,8 +5541,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: @@ -6388,8 +5892,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.0.2: {} - json-parse-better-errors@1.0.2: {} json-parse-even-better-errors@3.0.0: {} @@ -6412,8 +5914,6 @@ snapshots: dependencies: indento: 1.1.13 - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -6458,12 +5958,6 @@ snapshots: lru-cache@11.1.0: {} - 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 @@ -6574,8 +6068,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 @@ -6795,12 +6287,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 @@ -6820,17 +6306,6 @@ snapshots: queue-microtask@1.2.3: {} - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react-is@17.0.2: {} - - react-refresh@0.14.2: {} - - react@19.2.7: {} - read-pkg@3.0.0: dependencies: load-json-file: 4.0.0 @@ -6983,12 +6458,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: {} @@ -7276,8 +6747,6 @@ snapshots: dependencies: tldts-core: 7.0.19 - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7419,12 +6888,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: @@ -7455,15 +6918,6 @@ snapshots: jiti: 2.4.2 terser: 5.46.0 - vitest-browser-react@2.2.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.10): - dependencies: - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - vitest: 4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.12.14(@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.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - vitest@4.1.10(@types/node@20.19.43)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(msw@2.12.14(@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 @@ -7616,8 +7070,6 @@ snapshots: y18n@5.0.8: {} - yallist@3.1.1: {} - yallist@5.0.0: {} yaml-ast-parser@0.0.43: {} From d214fa0ca85180aa6d50482f8386c147587be115 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/5] 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 154e4cc2fd..745d616afe 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 @@ -302,7 +302,7 @@ sandboxTest.skipIf(isDebug)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate running sandboxes', async ({ sandbox, sandboxTestId }) => { // Create extra sandboxes @@ -338,7 +338,7 @@ sandboxTest.skipIf(isDebug || !canReadPaginationToken)( } ) -sandboxTest.skipIf(isDebug || !canReadPaginationToken)( +sandboxTest.skipIf(isDebug)( 'paginate paused sandboxes', async ({ sandbox, sandboxTestId }) => { await Sandbox.betaPause(sandbox.sandboxId) @@ -377,7 +377,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 509c41c0dc..0ec764ee6e 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -157,17 +157,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 afe3fb8e0bcfe445185a27d87b069cff61bbd081 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/5] 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 0ec764ee6e..61086f4af6 100644 --- a/packages/js-sdk/tests/setup.ts +++ b/packages/js-sdk/tests/setup.ts @@ -149,25 +149,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 4c94fca30bec6e36b5b963bee6b13c377a3b17d0 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/5] 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 bf5d7cf59cfcc72377b838b20ae98ff84a5f43e6 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/5] 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 6e1e245e5c..008d785852 100644 --- a/packages/js-sdk/tests/sandbox/network.test.ts +++ b/packages/js-sdk/tests/sandbox/network.test.ts @@ -1,7 +1,7 @@ import { assert, expect, describe } from 'vitest' import { CommandExitError } from '../../src' -import { sandboxTest, isDebug, canFetchSandboxServers } from '../setup.js' +import { sandboxTest, isDebug, corsHttpServerCmd } from '../setup.js' describe('allow only 1.1.1.1', () => { sandboxTest.override({ @@ -129,7 +129,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 @@ -137,7 +137,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, }) @@ -171,12 +171,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 61086f4af6..c5cc38e269 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 { @@ -146,17 +145,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()