What the agent does
The agent starts with the Jest configuration, because that is where a migration is won or lost. In a React project like react-hook-form/react-hook-form, scripts/jest/jest.config.js defines two projects: Web, which matches **/__tests__/**/*.(spec|test).ts?(x), transforms TypeScript with @swc/jest, runs in the jest-fixed-jsdom environment with customExportConditions set to an empty string, and loads scripts/jest/setup.ts; and Server, which matches *.server.(spec|test).ts?(x) and runs in Node. Both set clearMocks, resetMocks and restoreMocks to true, and the root config lists collectCoverageFrom. package.json runs tests with jest --config ./scripts/jest/jest.config.js on pnpm, with TEST_ENV=web selecting one project, and tsconfig.json puts jest and @testing-library/jest-dom in types. The CI job in .github/workflows/build-test.yml runs pnpm run test --ci and then pnpm test:type.
Then it measures what the tests use before touching them. A grep across src counts the Jest calls that need a replacement: jest.fn, jest.spyOn and jest.mock map to vi, jest.useFakeTimers and jest.advanceTimersByTime to Vitest's timer API, jest.requireActual to the asynchronous vi.importActual, and the jest.Mock and jest.MockedFunction types to Vitest's Mock type. It also counts the snapshot files under __snapshots__, since Vitest formats snapshots differently by default and a mass rewrite of snapshots would hide a real change. The agent runs the Jest suite once first and keeps the total as the number the migration must match.
The new vitest.config.ts keeps the shape of the old one: two entries under projects, one with environment: 'jsdom' and the setup file, one with environment: 'node', the same include globs, clearMocks, mockReset and restoreMocks turned on to match, and snapshotFormat set so the existing snapshots still match. The setup file switches to @testing-library/jest-dom/vitest. Transform needs no plugin: Vitest compiles TypeScript itself, so @swc/jest goes. The jest-fixed-jsdom environment exists to repair globals that Jest's jsdom breaks for fetch and MSW; the agent checks whether any test depends on that repair under Vitest's jsdom before dropping it, and says what it found.
Then the mechanical part, file by file, with the suite run between batches: vi for jest, await vi.importActual where jest.requireActual was synchronous, vi.useFakeTimers() and vi.advanceTimersByTime(), and vitest/globals replacing jest in tsconfig.json's types. package.json loses jest, @swc/jest, @types/jest, jest-environment-jsdom and jest-fixed-jsdom and gains vitest and jsdom; pnpm-workspace.yaml lists esbuild under allowBuilds as false, so the agent confirms Vitest still starts with that setting before it finishes. The last run has to report the same number of tests as the first, and pnpm test:type and pnpm lint have to pass as CI runs them.
Start it with one API call
POST https://api.prix.dev/o/{org}/sessions with this body:
{
"agent": "claude",
"input": "Migrate this project's tests from Jest to Vitest. Read package.json, pnpm-workspace.yaml, tsconfig.json, scripts/jest/jest.config.js, scripts/jest/setup.ts and .github/workflows/build-test.yml first, run the Jest suite once and record the total test count. Write a vitest.config.ts that keeps the two projects, their environments, include patterns, setup file, mock reset settings and coverage includes, and a snapshotFormat that keeps the existing snapshots valid. Replace jest.fn, jest.spyOn, jest.mock, jest.requireActual, the fake timer calls and the Jest mock types with their Vitest equivalents, switch the setup file to @testing-library/jest-dom/vitest, and replace jest with vitest/globals in tsconfig types. Remove Jest, @swc/jest, @types/jest and the Jest environments from package.json and the lockfile, update the test scripts and the CI workflow, and make the suite pass with the same count. Run pnpm lint and pnpm test:type. Open a pull request with the before and after counts and anything that needed a different approach under Vitest.",
"resources": [
{
"url": "https://github.com/your-org/your-app"
}
]
}- Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
- Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
- Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.
What comes back
What comes back is a pull request on its own branch: vitest.config.ts, the edited setup file, the test files with vi in place of jest, tsconfig.json, package.json and pnpm-lock.yaml with Jest gone, and the Unit Test / Lint workflow pointing at the new script. scripts/jest/ is removed. The description gives the Jest total from the first run and the Vitest total from the last, lists every file where a call needed more than a rename, and says whether the snapshots were kept byte for byte.
The transcript keeps the first Jest run, each Vitest run between batches with its failures, and the final run, so a reviewer can see when the count matched and what changed in between. The repository's own workflow runs the new script on the pull request, which is the check that matters for merging.
Before merging, a reviewer looks at three things: that no test was skipped or deleted to reach the count, that restoreMocks and the other reset options match the old config so tests do not start leaking state into each other, and that the snapshot files are unchanged or changed for a reason the description gives. One follow-up message to the same session asks for a different coverage provider or a watch script, and the sandbox runs the suite again.
Run it on every migration issue opened
When a maintainer opens an issue titled for the migration, an automation with a GitHub trigger on the issue event starts this run with the issue text after the prompt. Only work by an owner, member or collaborator of the repository fires it, so an outside account cannot start a run by opening an issue.
The same request can come from your own server with the repository in resources. Set a webhook_url on the API key and Rush POSTs run.completed when the turn finishes, with the session_url that holds the pull request and the final test count.
The use case this job follows, with its automation and trigger: Hand off from your terminal.
Why run it on Rush?
A migration like this is a long tail of small edits with a test run after each batch, and the runs are where the time goes. On a laptop the suite competes with the editor and the browser for the afternoon. In a Rush sandbox the agent installs with pnpm once, runs the suite as many times as it takes, and the sandbox parks with its disk and memory when the turn ends, so you can send a follow-up tomorrow without reinstalling.
The pull request comes with proof that the count matched: the first Jest run and the last Vitest run are both in the transcript, with every failing run between them. A reviewer does not have to trust that the migration kept the suite intact, and the project's own workflow runs the new command on the pull request before anyone merges.
Frequently asked questions
Do the two Jest projects become two Vitest configs?
No. Vitest's projects option takes both in one vitest.config.ts, each with its own environment, include pattern and setup file, so TEST_ENV=web style selection becomes a --project flag. The coverage include list moves to the root of the same file.
What happens to the snapshots?
The agent sets snapshotFormat so Vitest prints snapshots the way Jest did, then runs the suite without updating them. A snapshot that still fails is reported in the pull request with the diff rather than overwritten, because an updated snapshot can hide a real rendering change.
Why does jest.requireActual need more than a rename?
Because vi.importActual returns a promise. Each call becomes an await inside an async factory passed to vi.mock, which is a structural change the agent makes by hand in the files where the grep found it, and names in the description.
Does it work with pnpm and the workspace's allowBuilds setting?
Yes. The agent installs with the package manager the lockfile belongs to and keeps the lockfile in the same commit. If pnpm-workspace.yaml blocks a dependency's install script, the agent runs Vitest once to confirm it starts under that setting and says so in the pull request.
Will it migrate the Playwright end-to-end tests too?
No. The e2e script runs Playwright and is unrelated to Jest; it stays as it is. The migration covers the unit and server tests the Jest config matched.