Skip to content

fix(vitest): run app tests without starting the Worker - #762

Merged
cpojer merged 3 commits into
voidzero-dev:mainfrom
morinokami:fix/vitest-app-tests
Nov 9, 2026
Merged

cpojer merged 3 commits into
voidzero-dev:mainfrom
morinokami:fix/vitest-app-tests

Conversation

@morinokami

@morinokami morinokami commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

vp test stops with a startup error before it runs any test in a Void app on the default Cloudflare target. A fresh app shows it with a single expect(1).toBe(1) test:

npm create void@latest my-app -- --toolchain vite-plus --framework react --starter sqlite --deploy none --no-git
ReferenceError: module is not defined
    at runInRunnerObject (./workers/runner-worker/index.js:107:3)
    at getWorkerEntryExportTypes (./workers/runner-worker/index.js:252:24)

With nodejs_compat, which Void also turns on for apps that use auth, PostgreSQL, MySQL or Sandbox, and for TanStack Start, React Router and vinext apps:

Error: AssertionError: depsOptimizer is required in dev mode

vp dev works. Vitest loads vite.config.ts and starts a Vite server of its own, so the Cloudflare Vite plugin that voidPlugin() includes starts the Worker there too. Vitest turns off dependency prebundling in nearly every environment of that server, the Worker's included, and the Cloudflare plugin relies on it in development: a CommonJS dependency such as React reaches workerd unconverted, and with nodejs_compat the plugin's own polyfill import asserts on the missing optimizer. Users can't remove the plugin, and test.deps.optimizer doesn't bring dependency discovery back. The only workaround was a vitest.config.ts without voidPlugin(), which loses @schema and Void's generated modules, so the starter's pages/index.server.ts can't be imported.

Vitest's server also ran dev-server work. Each vp test applied local migrations, for PostgreSQL and MySQL to the database in .env, and in an app that uses AI without a linked project it would start the interactive project setup in a terminal.

Fix

isVitestServer() recognizes the server that Vitest creates by the plugins Vitest adds to it (vitest:*). In that server:

  • The plugins of Void's Cloudflare wrapper, the Cloudflare Vite plugin included, don't apply, in Void apps and in TanStack Start, React Router and vinext apps. Void's other plugins still resolve void/*, @schema and the generated modules, so tests can import route handlers and page loaders.
  • migrationPlugin doesn't apply.
  • The AI project setup prints its hint instead of prompting.

Code generation, types and warnings stay as they are. Tests run in Node.js, so a call that reaches a binding still throws env: Cloudflare env is unavailable. Running tests in workerd with real bindings, as @cloudflare/vitest-plugin does for plain Workers, would need Void's generated entry, inferred bindings and migrations in the test runtime, and is left for later.

Vite passes each plugin's apply the config of the server it resolves, Vitest's plugins included, so the check works in any --mode and only for Vitest's own server. The alternatives fail in worse directions:

Check Misses Wrongly matches
process.env.VITEST Servers from Vitest's API: createVitest() doesn't set it Every server in the process, such as a dev server that a test starts, and Void's own Cloudflare plugin tests
mode === 'test' vitest --mode staging vp dev --mode test, which would lose the Worker
Vitest's plugin names A future rename, which falls back to today's error None found

The check also accepts the plugin named vitest that Vitest 3.2.4 and 4.0.0 add. The integration test below fails if a later Vitest renames these plugins, and packages/void/AGENTS.md asks to check the names when updating Vitest.

Docs

  • A Testing guide under Development & Deployment: vp test needs no configuration, test APIs come from vite-plus/test (vitest with the Vite toolchain), test files stay out of convention directories (a test file in routes/ becomes a route), and vi.mock() replaces a binding module, since bindings aren't available in tests.
  • The email testing examples import from vite-plus/test and link to the guide.
  • The Void skill lists the guide, and TESTING.md mentions the packed vp test check.

Tests

  • Unit: isVitestServer(); the Cloudflare wrapper resolved with and without Vitest's plugins, in modes test and staging; migrationPlugin in real dev servers; and the AI setup prompt.
  • Integration: packages/void/test/integration/vitest-app.test.ts runs vp test run in isolated copies of playground/react-pages, first as is and then with nodejs_compat and --mode staging, and of playground/tanstack-start. It checks that no migration runs and no local D1 database appears. Without this fix, it fails with both errors above.
  • Packed consumer: the Vite+ fixtures run vp test run on the guide's vi.mock('void/kv') example and a page loader import. Because this changes the packed script, this PR runs the full packed suite.

Verification

  • vp test run: 790 files pass, 1 skipped (16,479 tests pass, 30 skipped).
  • vp run check: 33/33 pass.
  • vp run build:docs passes.
  • By hand: vp test run passes in copies of the TanStack Start, React Router, vinext and SvelteKit playgrounds. In a copy of playground/react-pages, the guide's vi.mock('void/kv') example and the email testing example pass with vp test run and with plain vitest run, and no local D1 database appears. vp dev still serves / and /api/health and applies the migration.
  • By hand on an app scaffolded from the packed tarballs with the command above (Vite+ 1.5.0, Vitest 5.1.0): vp test run passes with and without nodejs_compat.
  • Not run: the packed consumer script locally. On my machine, the interactive void init stalled in its PTY while installing dependencies, before the new step.

🤖 Generated with Claude Code

morinokami and others added 3 commits November 9, 2026 21:47
`vp test` stopped with a startup error before running any test in Void
apps on the default Cloudflare target, including a fresh
`npm create void` app:

    ReferenceError: module is not defined

With `nodejs_compat`, which Void turns on for auth, PostgreSQL, MySQL,
Sandbox, and TanStack Start, React Router and vinext apps:

    Error: AssertionError: depsOptimizer is required in dev mode

Vitest loads `vite.config.ts` and starts a Vite server of its own, so
the Cloudflare Vite plugin that `voidPlugin()` includes started the
Worker there too. Vitest turns off dependency prebundling in nearly
every environment, the Worker's included, and the Cloudflare plugin
relies on it: a CommonJS dependency such as React reached workerd
unconverted, and the plugin's own nodejs_compat polyfill import
asserted on the missing optimizer. Users could not remove the plugin,
and `test.deps.optimizer` does not bring discovery back.

## Fix

`isVitestServer()` recognizes the server that Vitest creates by the
plugins Vitest adds to it (`vitest`, `vitest:*`), in any `--mode`. A
server that a test starts through Vite's API, and `vp dev --mode test`,
are not Vitest's and keep everything.

In Vitest's server:

- The plugins of Void's Cloudflare wrapper, the Cloudflare Vite plugin
  included, do not apply, in Void apps and in framework apps. Void's
  other plugins still resolve `void/*`, `@schema` and the generated
  modules, so tests can import route handlers and page loaders.
- `migrationPlugin` does not apply, so a test run no longer migrates
  the local D1 database or the PostgreSQL or MySQL database in `.env`.
- The AI project setup prints its hint instead of prompting.

Tests run in Node.js, so a call that reaches a binding still throws
`env: Cloudflare env is unavailable`; `vi.mock()` can replace the
binding module.

## Tests

- Unit: the detection, the Cloudflare wrapper and the migration plugin
  with and without Vitest's plugins (modes `test` and `staging`), and
  the AI setup prompt.
- Integration: `vp test` in isolated copies of the React Pages
  playground (as is, with `nodejs_compat` and with `--mode staging`)
  and the TanStack Start playground.
- Packed consumer: Vite+ fixtures run `vp test` with a mocked binding.

## Docs

Add a Testing guide: `vp test` needs no configuration, tests run in
Node.js without bindings, and test files stay out of the convention
directories. Link it from the email testing sections and the Void
skill, and note the Vitest server in the package AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Testing guide imports test APIs from `vite-plus/test`, the Vite+
toolchain's entry for Vitest, and names `vitest` for the Vite
toolchain. The email testing examples still imported from `vitest`,
which TypeScript cannot resolve in a pnpm-installed Vite+ app that
does not install Vitest directly. Import from `vite-plus/test` there
too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unit test that starts Vite dev servers to check where migrations
apply needs no file watching. Pass `watch: null`, as the other unit
tests that start a dev server do, so no native file watcher runs in
the unit project's worker threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@morinokami
morinokami requested a review from a team as a code owner November 9, 2026 13:05
@cpojer
cpojer self-requested a review November 9, 2026 22:35

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@cpojer
cpojer merged commit 1003bc9 into voidzero-dev:main Nov 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants