$npx -y skills add rianvdm/product-ai-public --skill workers-vitest-poolSet up or upgrade vitest tests for Cloudflare Workers using @cloudflare/vitest-pool-workers. Use when scaffolding tests in a new Worker, when an existing Worker is on pool-workers <0.13 (vitest 2/3) and needs to upgrade, or when the test suite hits "Isolated storage failed" / "Ex
| 1 | # Workers vitest pool: known-good setup + v2→v4 upgrade recipe |
| 2 | |
| 3 | Amazon's `@cloudflare/vitest-pool-workers` had a long-lived bug in the `0.5.x` line where the per-test storage isolation snapshot asserts every persist file ends in `.sqlite`, but SQLite-backed Durable Objects in WAL mode produce `.sqlite-shm` and `.sqlite-wal` files alongside. The bug manifests as: |
| 4 | |
| 5 | ``` |
| 6 | Failed to pop isolated storage stack frame in <test name>'s test "<...>". |
| 7 | In particular, we were unable to pop Durable Objects storage. |
| 8 | - AssertionError [ERR_ASSERTION]: Expected .sqlite, got .../foo.sqlite-shm |
| 9 | ``` |
| 10 | |
| 11 | Upstream fix landed in **`@cloudflare/vitest-pool-workers@0.13`**, which requires **vitest 4** and **Node 22+**. This skill captures the canonical setup and the migration recipe — battle-tested in `tldl` (PR #39, 2026-05-02). |
| 12 | |
| 13 | ## When to use this skill |
| 14 | |
| 15 | - **Scaffolding tests in a new Worker.** Use the "Known-good setup" section to pin the modern stack from day one. |
| 16 | - **An existing Worker is on pool-workers `0.5.x` / `0.6.x` / `0.7.x` / `0.8.x` / `0.9.x` / `0.10.x` / `0.11.x` / `0.12.x`.** Even if tests appear to pass, the bug is latent — any test that uses a SQLite-backed Durable Object will hit it eventually. Run the migration. |
| 17 | - **Tests are reporting "Isolated storage failed" or "Expected .sqlite, got .sqlite-shm".** That is exactly this bug. |
| 18 | |
| 19 | ## Known-good setup (use this for new projects) |
| 20 | |
| 21 | ```jsonc |
| 22 | // package.json — devDependencies |
| 23 | { |
| 24 | "@cloudflare/vitest-pool-workers": "^0.15", |
| 25 | "@cloudflare/workers-types": "^4", |
| 26 | "@types/node": "^22", |
| 27 | "typescript": "^5", |
| 28 | "vitest": "^4", |
| 29 | "wrangler": "^4" |
| 30 | } |
| 31 | ``` |
| 32 | |
| 33 | ```jsonc |
| 34 | // tsconfig.json — types entry |
| 35 | { |
| 36 | "compilerOptions": { |
| 37 | "types": [ |
| 38 | "@cloudflare/workers-types", |
| 39 | "@cloudflare/vitest-pool-workers/types", // ← /types subpath, not bare |
| 40 | "node" |
| 41 | ] |
| 42 | } |
| 43 | } |
| 44 | ``` |
| 45 | |
| 46 | ```ts |
| 47 | // vitest.config.ts — v4 shape |
| 48 | import { cloudflareTest, readD1Migrations } from "@cloudflare/vitest-pool-workers"; |
| 49 | import { defineConfig } from "vitest/config"; |
| 50 | import path from "node:path"; |
| 51 | |
| 52 | const migrations = await readD1Migrations(path.join(import.meta.dirname, "migrations")); |
| 53 | |
| 54 | export default defineConfig({ |
| 55 | plugins: [ |
| 56 | cloudflareTest({ |
| 57 | wrangler: { configPath: "./wrangler.toml" }, |
| 58 | miniflare: { |
| 59 | bindings: { |
| 60 | // test-only env vars + secret stand-ins |
| 61 | }, |
| 62 | }, |
| 63 | }), |
| 64 | ], |
| 65 | test: { |
| 66 | provide: { DB_MIGRATIONS: migrations }, |
| 67 | }, |
| 68 | }); |
| 69 | ``` |
| 70 | |
| 71 | ```text |
| 72 | // .nvmrc |
| 73 | 22 |
| 74 | ``` |
| 75 | |
| 76 | ```yaml |
| 77 | # .github/workflows/ci.yml |
| 78 | name: CI |
| 79 | on: |
| 80 | push: |
| 81 | branches: [main] |
| 82 | pull_request: |
| 83 | jobs: |
| 84 | test: |
| 85 | runs-on: ubuntu-latest |
| 86 | steps: |
| 87 | - uses: actions/checkout@v4 |
| 88 | - uses: actions/setup-node@v4 |
| 89 | with: |
| 90 | node-version: 22 |
| 91 | cache: npm |
| 92 | - run: npm ci |
| 93 | - run: npm run typecheck |
| 94 | - run: npm test |
| 95 | ``` |
| 96 | |
| 97 | ## Per-test storage isolation: don't rely on it |
| 98 | |
| 99 | With the known-good `cloudflareTest` config above, D1 / KV / R2 state is **NOT reset between `it()` blocks** — it persists across every test in a file (storage behaves as per-file, not per-test). The package's "isolated storage" reliably covers only Durable Objects; relational / KV / R2 writes accumulate. |
| 100 | |
| 101 | So tests must not depend on a clean slate. Either: |
| 102 | |
| 103 | - **Use unique identifiers per test** — emails, display names, codes, any natural key randomised per `it()` (e.g. `t-${Math.random()}@x.test`). A test that signs up a fixed `t-fixed@x.test` makes every later test in the file that reuses it fail with an unexpected `409`. |
| 104 | - **Or add a `beforeEach` reset** when tests genuinely need an empty DB — truncate the D1 tables in FK-safe (child→parent) order, then drain the KV namespace and R2 bucket by paginating `list()` + `delete()`. |
| 105 | |
| 106 | Symptom when this bites: the *first* test in a `describe` passes and every later one fails — typically a `409`/`401` where a `2xx`/`403` was expected, because a fixed-identity signup now collides with a row a prior test left behind. Discovered 2026-05-19 on the Therapy Space backend: a 22-test security-sweep file with fixed identifiers had 12 failures until a `beforeEach` reset was added. |
| 107 | |
| 108 | ## Migration recipe (vitest 2 + pool-workers 0.5 → vitest 4 + pool-workers 0.15) |
| 109 | |
| 110 | Apply these in one branch. Order matters — config first, then types, then test casts, then latent bugs. |
| 111 | |
| 112 | ### 1. Bump devDependencies |
| 113 | |
| 114 | ```bash |
| 115 | npm install -D vitest@^4 @cloudflare/vitest-pool-workers@^0.15 @types/node |
| 116 | echo 22 > .nvmrc |
| 117 | ``` |
| 118 | |
| 119 | Pool-workers 0.15+ requires **vitest ^4.1** and ** |