Start with the smallest relevant test file or case while developing:
vp test run packages/void/test/unit/plugin-routing.test.ts
vp test run packages/void/test/unit/plugin-routing.test.ts -t 'routing'
vp run checkType-only contracts use *.typecheck.ts. Package typechecking verifies them; the Vitest runtime
does not need to execute their expectTypeOf assertions.
vp run test is the local affected-tests default; vp test run, vp run test:full, and CI still
run the full suite. The selector's result caching is disabled: Git state must be inspected on every run.
Do not pass vp run --cache to this task.
vp run test --list # Preview files and fallback reasons; no builds or test hooks
vp run test # Staged, unstaged, and untracked changes against HEAD
vp run test --base origin/main # Branch commits since merge-base, plus local changes
vp run test --unit # Faster iteration only; explicitly skips integration projectsPass shortcut arguments directly after the task name, without an extra --. A clean working tree
selects nothing unless --base includes earlier commits. Invalid Git refs or selection failures
fail the command rather than silently skipping coverage.
The runner uses Vitest's static import graph, including transitive and literal dynamic imports.
It supplies changed paths through Vitest's experimental.vcsProvider hook.
Independent Git queries run asynchronously in parallel, and filtering reuses one snapshot per invocation.
Neither test results nor Git state are cached between commands. Built public exports join that snapshot
before import-based selection. The hook is specific to the smart runner: native vp test run --changed
still lacks the repository's integration fallbacks.
The runner adds coverage that imports cannot describe:
- SDK source changes rebuild public packages, select all SDK build/server/browser integrations, and include built public exports in the related graph so platform and adapter consumers are covered.
- Platform source, migration, and fixture changes select the owning package's tests, including both API integration worker pools. Ordinary changed test files use the native related graph.
- Playground changes select suites mentioning that fixture path. An unmapped playground selects all SDK integrations; shared test tooling, configuration, dependencies, deletions/renames, and unknown ownership fall back to the full selection.
- Documentation-only changes skip runtime tests. Verify documentation rendering/examples separately.
--list does not refresh builds, so built-export coverage can expand when the command executes.
Affected selection is conservative, not a proof that all behavior is covered: computed imports,
filesystem reads, generated artifacts, and external services can introduce additional dependencies.
Explicitly run a known consumer when changing one of those boundaries. Changes to packaging still
need the packed checks below; changes to metaframework output still need build:all and the
artifact check.
For a leaf-module edit with known import-based consumers, Vitest's native related command is useful:
vp test related --run packages/void/src/runtime/sse.ts --project unit --project unit-pglite --project cli-unit
vp test list packages/void/test/unit/plugin-routing.test.ts --filesOnlyAgents should run a focused regression first, then affected verification and vp run check after a
batch. Use unit-only runs during iteration, but run the affected integration selection before
handoff. Run the full suite once for final verification, not after every edit. For interactive
development, vp test watch <test-file> keeps the transform graph in memory between edits.
The named shortcuts select the current test projects:
vp run test:unit
vp run test:integration
vp run test:metaframeworks
vp run test:pages:react-vue
vp run test:pages:svelte-solid
vp run test:platformRun the complete SDK and platform verification before handing off a finished change:
vpr test:allvpr test:all formats, lints, builds, typechecks, and tests the merged monorepo. Investigate slow
or hanging tests when they occur instead of routinely increasing timeouts.
Subprocess and provisioning-lock unit tests start alongside the platform projects so their real
process deadlines can overlap database work. The remaining SDK unit and integration files then
share a scheduling phase. vp run test:unit includes unit, unit-pglite, and cli-unit; CI does the same.
Unit files and most API integration files use isolated worker threads to reduce startup time. On Linux and macOS,
API integration files and the PGlite unit files (unit-pglite, platform-unit-pglite) use forks to isolate PGlite's
WebAssembly JIT allocations after native Node 24 crashes in shared worker processes. API tests
that change the process timezone also use forks, because threads cannot change Node's timezone. Select every
API file with vp test run --project 'platform-api-integration*'. SDK integration files use forks.
This keeps database engines from competing with all framework compilers at startup. Keep module
isolation enabled: these suites mock globals, keep database state, and exercise real process/lock boundaries.
The full verification command builds before running the platform's type-aware lint, so it also
works without a pre-existing dashboard .void/tsconfig.json.
Persistent fsModuleCache is enabled to reuse transformations across invocations. It does not
cache test results or remove per-file module isolation. The first run primes it; compare repeated
runs with the same selection and machine load. For a control use --no-fsModuleCache; use
vp test --clearCache only when diagnosing cache problems, not before ordinary tests.
Vitest 5's import diagnostics are opt-in so ordinary runs stay quiet:
vp run test:profile packages/void/test/unit/plugin-routing.test.ts
vp run test:profile --project 'platform-api-integration*' api-delete-accounting.test.ts
vp test doctor packages/void/test/unit/plugin-routing.test.tsThe profile shortcut enables experimental.importDurations.print and reports the top 20 imports
with the default reporter. Use --experimental.importDurations.print=verbose or =tree when more
detail is useful. Self is work attributed to the module; Total includes imported dependencies,
so do not add totals together. Prefer narrow entry points and production lazy loading when an
optional feature brings in a heavy graph. Do not replace production imports with mocks just to
make the measurement smaller. See Vitest's profiling guide.
If import/transform time dominates, inspect import diagnostics or a main-thread CPU profile. If
test time dominates, investigate real code, database setup/reset, server readiness, and teardown.
Worker phase percentages are aggregated and are not independent wall-clock savings. doctor
executes its selection multiple times; never run it on the whole monorepo as a routine agent step.
Its suggestions require correctness checks: this repository's native dependencies and timezone
tests rule out blanket VM pools, and shared mocks/state require keeping isolation enabled.
CI builds and typechecks every playground once per OS in a dedicated required job. Integration
shards build the public packages and then only the fixtures their tests use; they do not depend
on generated output from another job. The done check requires both the tests and build/type
checks. Linux and Windows coverage and the credentialed live-deploy gate are unchanged.
The build job runs test/check-playground-build-artifacts.mjs after build:all to verify the
metaframework worker entries and inferred bindings. Their dev-server integration suites do not
repeat those production builds. vpr test:all runs the same artifact check locally.
Integration sharding uses measured duration estimates, rather than filename hashes. The sequencer packs the longest files first and starts costly fixtures early. Estimates affect scheduling only: new files receive a fallback estimate. Refresh the estimates when the suite's costs change materially. Seeded file shuffling remains available for isolation checks.
The four adapter suites share their client and island test definitions in test/. Each runs the
full browser behavior contract against its production preview build, plus a dev-mode navigation,
island hydration/interaction smoke check, SSR/protocol assertions, and HMR checks. This keeps both
runtime modes covered without repeating the full browser matrix.
Use production functions in unit tests. A copy of a hook's implementation inside a test cannot catch regressions in the real hook. Shared form-value equality has one direct suite, and actual form interactions run in the adapter browser matrix.
The in-memory API deletion tests schedule the fixed container-orphan sweep delay immediately. They still execute both sweeps and all provider/database assertions. Production timing and the real subprocess/lock deadline tests retain their actual timing semantics.
API integration fixtures reset their tables with one ordered SQL block. Use clearTables rather
than repeating per-table cleanup; it keeps foreign-key delete behavior and migration history
intact in PGlite and PostgreSQL.
The cached PGlite database template persists a UTC session default, independently of the
creating or consuming process's timezone. Increment TEMPLATE_VERSION in
platform/packages/api/test/integration/db-template.ts when changing template configuration
so existing local dumps are rebuilt. For database-clock assertions, compare timestamps in SQL;
JavaScript interprets timestamps without a timezone as local time. Keep explicit non-UTC
regressions for behavior that must work across timezones.
Platform CI runs the candidate's PostgreSQL concurrency cases in a separate job. The selector
finds API integration files that read process.env.VOID_TEST_DATABASE_URL, excluding mentions
in comments or strings. Each selected file runs against its own database clone, with two files
in flight, using the same bounded runner as schema compatibility. This exercises real locks
without repeating the entire API suite. To run it locally, migrate a disposable loopback
void_schema_compat database as void_ci, then set COMPAT_DATABASE_URL and run
node .github/scripts/postgres-concurrency-tests.ts.
Staging CI on main also runs the production schema-compatibility gate, so its failures are caught
before tagging a release. The job copies the candidate's .github/scripts/schema-compat-tests.ts runner
before checking out the deployed revision. It runs every deployed API test file against its own
clone of the upgraded PostgreSQL database, with two files in flight. Separate databases
prevent cross-project cleanup races and leaked fixture tables while preserving real multi-connection
tests within each file. The original upgraded database and its seeded compatibility records remain
untouched. The runner accepts only the disposable loopback void_schema_compat database as void_ci.
It also repairs two legacy fixtures, each gated by its full file checksum: the email concurrency
fixture must return the same zone ID for both lookups and model DNS for whichever request wins,
and the oversized deploy-body fixture must
consume its response before the next test clears its tables. Deployed runtime source and all test
assertions stay intact. Unknown revisions are left untouched, so a new failure cannot silently
inherit an old fixture exception.
CLI and deploy tests that stub their network boundary and call production functions directly run in the unit project. The integration project is reserved for real builds, subprocesses, servers, and browser behavior.
Packed tests install the current tarballs into fresh applications through a temporary local registry. They catch dependency, export, type-declaration, and installed-runtime problems that workspace links can hide. They do not publish packages or deploy applications.
| Change or trigger | Packed coverage |
|---|---|
| Documentation only | Skip install/build/browser jobs |
| Routine code changes | One React app with each of npm and pnpm |
| Package manifests, lockfiles, packaging configuration, adapters, scaffolding, or packed-test tooling | Full four-framework matrix |
| Release tags | Full matrix on Linux and macOS |
| Manual workflow run | Choose smoke or full |
Smoke coverage checks package contents and exports, real initialization, CLI help, a frozen reinstall, public Vite builds, TypeScript, and development HTTP requests for pages, assets, and KV. It uses no browser. Full coverage adds Vue, Svelte, Solid, Markdown, production preview, and browser hydration/navigation. Superseded PR/main runs are cancelled; release runs are not.
The selector reads all PR file pages or the complete pushed commit range. Renames consider both paths. Incomplete or unavailable change lists fall back to full coverage.
Run these checks on Linux or macOS:
vp run build
node .github/scripts/packed-consumer.mjs
VOID_PACKED_SUITE=full node .github/scripts/packed-consumer.mjs
node --test --test-timeout=10000 .github/scripts/packed-*.test.mjsVOID_PACKED_PM=npm|pnpm and VOID_PACKED_FRAMEWORK=react|vue|svelte|solid select a focused
local run. VOID_PACKED_TOOLCHAIN=vite-plus verifies Vite+ initialization, the workspace's
tested Vite+ version, and the generated vp build/dev/preview commands. CI checks a React
app with Vite+ for both package managers alongside the plain Vite coverage.
The interactive initializer is a Node.js script using the system script PTY
utility and Bash; it has no Python or native npm dependency.
Pages scroll regressions run in Chromium by default. On a machine with Playwright WebKit support, run the same document navigation, reload, SPA navigation, and suspended-render cases in WebKit:
vp exec playwright install webkit
VOID_TEST_BROWSER=webkit vp test run packages/react/test/integration packages/vue/test/integration packages/svelte/test/integration packages/solid/test/integration -t 'restores.*scroll'Use waitForIslandsHydrated from test/browser-helpers.ts before asserting island state or
interacting with islands. Pass the expected minimum count when checking a multi-island fixture.
Each island imports and hydrates independently, so a selector matching one hydrated island does
not establish that its siblings are ready. The helper rejects empty or partially hydrated pages.
For SPA navigation, wait for a client readiness signal before setting the document marker and
clicking. The Nuxt and SvelteKit playgrounds expose h1[data-hydrated="true"] after client startup.
networkidle and server-rendered HTML do not establish router readiness. Keep the document-marker
assertion so a full reload still fails the test. These suites attach recent browser console,
navigation, failed-request, HMR, and dev-server output to failures.
Give real subprocess tests a measured, suite-local timeout. The Drizzle drift suite can launch
three processes per case and uses 30 seconds on Windows and 10 seconds elsewhere. Ordinary unit
tests retain their default timeout. waitForServer already scales its timeout on Windows;
callers should pass an unscaled value and leave enough hook time for that wait and setup.
Start integration servers with startServer from test/integration-helpers.ts, and pass
--strictPort to Vite. A server binds its port seconds after the port is chosen, so another
process can take it first. Vite without --strictPort then moves to the next port, which can be
another test's port. With it, the server exits and startServer starts it again on a new port.
Another process that took the port can answer on it too, so answers count only after the server
prints its own URL with the port (Local: http://…:<port>/ or Listening on http://…:<port>).
For a server that prints no URL, pass printsUrl: false and an expectBody only that app serves.
Windows integration tests also use maxConcurrency: 2. maxWorkers caps files, not concurrent
tests inside each file: two CI workers with Vitest's default concurrency of five can open ten
isolated browser contexts at once. The Windows cap reduces that to four without serializing
files or changing Linux/macOS concurrency. Do not retry browser navigation to hide
ERR_NO_BUFFER_SPACE; it reports a Windows socket resource failure, not a readiness timeout.
The Windows setup hook prints one resource snapshot per integration file at the first failed test, before suite teardown. It records memory, the largest processes, dynamic TCP port ranges, and connection counts by state/PID. Collection is read-only, limited to ten seconds and 64 KiB, and retains partial output if it fails. Concurrent failures share the snapshot. It does not log environment variables, process command lines, or remote addresses, and does not replace the original test failure. Use this evidence to distinguish port, queue, and memory pressure rather than assuming every socket error is port exhaustion.
Live tests are opt-in. They provision or update real resources and can write data, enqueue messages, invoke AI, and start scheduled work. They must not run as part of the ordinary local or CI test suite.
playground-local/ is ignored scratch space. It may contain an operator's account IDs, resource
IDs, Worker names, generated output, Access tokens, and an ad hoc validator. Those values are not
portable and must never be copied into tracked documentation or treated as the canonical E2E
setup.
Each operator should create their own fixture in playground-local/ or copy a tracked
playground/ app there. Keep all account-specific configuration in that ignored copy. Run deploy
commands from the fixture's project root; --dir selects a static output directory but does not
change the project root.
Pick a short namespace unique to the operator or test account, such as a team or username. Reuse stable names across runs:
void-e2e-<namespace>-static
void-e2e-<namespace>-kitchen
void-e2e-<namespace>-websockets
void-e2e-<namespace>-framework
void-e2e-<namespace>-sandbox
Use lowercase letters, numbers, and hyphens. Keep the namespace short because Cloudflare combines Worker and version-preview alias names under a 63-character hostname limit. Resource names should derive from the same namespace so the deploy adopts the operator's existing test resources instead of colliding with another operator.
Do not use a production Worker or production resource name. E2E automation must never delete Workers, databases, buckets, namespaces, queues, versions, or other remote resources. Live checks may leave uniquely named test records and objects behind. Any eventual cleanup must be a separate, explicit, manually reviewed operation against exact resource IDs.
Authenticate and inspect the accounts available to Void:
void cloudflare login
void cloudflare statusUse the intended test account rather than an account name embedded in a script. Pin it by either:
- letting
void initwriteaccount_idto the ignored fixture'swrangler.jsonc; or - setting
CLOUDFLARE_ACCOUNT_IDin the current shell.
When a version has no preview URL, also set CLOUDFLARE_WORKERS_SUBDOMAIN to the account's
workers.dev subdomain. For https://my-worker.my-team.workers.dev, the value is my-team. The
account name shown in the dashboard is not necessarily the workers.dev subdomain.
Never commit account IDs, resource IDs, API tokens, or Access credentials. CI should receive them through repository secrets or variables.
Prefer an unprotected or explicitly bypassed hostname for the baseline deployment suite. In an Access application, Allow + Everyone still requires the requester to authenticate. A genuinely public E2E hostname uses Bypass + Everyone and needs no Access credentials.
For a separate Access-protected readiness test:
- locally, set
CF_ACCESS_TOKENto the short-lived JWT returned bycloudflared access token --app=https://<worker>.<account-subdomain>.workers.dev; - in CI, use an admitted
CF_ACCESS_CLIENT_IDandCF_ACCESS_CLIENT_SECRETservice-token pair.
Void sends these credentials only to its guarded readiness request. Do not put them in
wrangler.jsonc, .env, fixture source, command-line arguments, or tracked files.
From the ignored fixture's root, initialize it for the operator's account and deploy through the same path a user runs:
void init
void deploy --platform cloudflareFor a static fixture whose output already exists:
void deploy --dir <output-directory> --platform cloudflareAt minimum, verify that the reported URL serves the new build and that a second deploy safely updates the same Worker. Static coverage should also exercise headers, redirects, rewrites, and 404 handling. A failed readiness check must leave or restore the previous production version.
Run deeper fixtures only after the baseline succeeds:
| Fixture | Expected coverage |
|---|---|
| Kitchen sink | SSR/hydration, D1, KV, R2, queues, cron, ISR, auth, and Workers AI |
| WebSockets | Durable Object migrations, typed WebSocket routing, and socket traffic |
| Framework | At least one detected third-party framework build and deployment |
| Static | Assets, headers, redirects, rewrites, ignores, and 404 handling |
| Sandbox | Containers and Sandbox runtime; paid-plan test only |
The default matrix must remain Workers Free-compatible. Do not run the Sandbox/Containers fixture unless the selected account is known to have Workers Paid and the operator intentionally opted into that test. An app that does not use a paid feature must not emit a paid binding or perform an entitlement probe.
Record the Worker names and URLs in ignored local state or pass them to a validator as environment variables. A reusable validator must derive hostnames from those inputs; it must not hardcode an employee's account ID, workers.dev subdomain, dated Worker names, or a token file path.