# Migrate a NestJS Project from Jest to Vitest

Send Rush a NestJS repository to move its tests from Jest to Vitest, keep the same tests passing, and get a pull request with before and after counts.

[Run this on Rush](https://cloud.getrush.ai/run/migrate-jest-to-vitest/nestjs/), in a Project that has your repository.

## What the agent does

In a NestJS project like lujakob/nestjs-realworld-example-app, the test setup is old enough that the migration is as much about era as about framework. package.json pins Jest ^25.5.3 with a hand-written jest.json: the transform is ts-jest/preprocessor.js and testRegex is /src/.*\.(test|spec).(ts|tsx|js)$. The whole suite is one file, src/tag/tag.controller.spec.ts, which builds a Nest testing module with Test.createTestingModule from @nestjs/testing and stubs the service with jest.spyOn. The app itself is NestJS 7 on TypeScript ^3.8.3 with TypeORM 0.2 against MySQL, npm is the documented installer, and there is no CI config to update.

The agent records the baseline with npm test, jest --config=jest.json, and captures the count. The catch is that the one spec boots TypeOrmModule.forRoot, so the suite needs a live MySQL even to run the tag controller test; the agent stands that up in the sandbox rather than discovering it mid-migration. Config moves to vitest.config.ts with an equivalent include pattern and path aliasing for the era's relative imports, and globals are enabled so describe, it and expect resolve without rewriting every file.

Conversion itself is small but precise. jest.spyOn becomes vi.spyOn, and the Nest testing module code is untouched because @nestjs/testing works under any runner that provides the globals it imports. The one thing Jest 25 allows that Vitest does not is silently relying on ambient jest types, so @types/jest leaves package.json and any place the types leaked into source is fixed rather than shimmed. The repo carries both package-lock.json and a stale yarn.lock that disagrees with the package version; the migration keeps the npm lockfile the README documents and leaves yarn.lock alone, noting the divergence in the pull request.

The proof is numerical: npm test before and after, same count, with the output in the description. Because there is no build script (production builds run tsc through prestart:prod), the agent adds a typecheck run to prove the removed Jest types broke nothing, and starts the app against MySQL as a smoke check that the Nest module graph still resolves. Jest, ts-jest and its preprocessor leave package.json, and the regenerated lockfile is committed alongside.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Migrate this project's tests from Jest to Vitest. Convert config, jest.* calls, module mocks, fake timers and snapshots. Make the same tests pass with the same count, remove Jest and its types, and open a pull request with the before and after test counts.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **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.
2. **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.
3. **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

The pull request contains vitest.config.ts, the converted spec, package.json without jest and ts-jest, and the regenerated package-lock.json. Its description gives the before and after test counts and the commands that produced them, plus the note about the stale yarn.lock. npm test now runs Vitest through the same script name, the typecheck passes without @types/jest, and the app still boots against MySQL.

A reviewer checks the count, then reads the converted spec to confirm the spy assertions survived intact. With a one-file suite there is nowhere for a dropped assertion to hide, but the principle is the same as a larger repo: same tests, same assertions, same count. The session read returns the repository and pull request information, and the transcript holds the baseline run, the converted run, the typecheck and the boot check.

## Run it on every migration issue opened

When an issue asks for a framework migration like this one, your automation sends POST /o/{org}/sessions with the issue text and this prompt, and the migration starts without a local setup. Install the Rush GitHub App on the repository first and authenticate with an API key; an Idempotency-Key header keeps a retried submission from starting two runs.

Keep the returned execution_id and read the session when the run settles, or receive the key's completion webhook. The before and after counts tell you the outcome at a glance; if the single spec had to change shape to run under Vitest, that is in the description, and a follow-up on the finished turn can ask the agent to justify it while the parked sandbox is still warm.

## Why run it on Rush?

Old Jest setups are the worst kind of migration to do locally: the suite may need a database, the tooling is a decade of drift, and the temptation is to stop after config compiles. On Rush the agent stands up MySQL in its own sandbox, runs the baseline and converted suites in the same place, and holds the output as evidence. You hand the job off from the terminal and read the result when it settles.

For several legacy services, submit one repository per run within your key's concurrency allowance and the fleet moves in parallel, each with its own database and baseline. You review each pull request for the same properties: equal counts, preserved assertions, no source changes. The useful proof is the two test runs side by side in the transcript.

## Frequently asked questions

### Does @nestjs/testing still work under Vitest?

Yes. The Nest testing module is runner-agnostic; it needs the globals it imports to exist, which is why the config enables them. The spec's Test.createTestingModule and module compilation are unchanged in the converted file.

### Why does the migration need MySQL?

The only spec boots TypeOrmModule.forRoot, so the suite connects to MySQL before it ever tests the tag controller. The agent provisions that in the sandbox and runs the baseline there, so the before and after numbers come from the same environment.

### What happens to the old ts-jest preprocessor config?

jest.json with ts-jest/preprocessor.js is replaced by vitest.config.ts with an equivalent include pattern and aliasing. Jest, ts-jest and @types/jest leave package.json, and a typecheck run proves nothing referenced the ambient jest types.

### Which lockfile is authoritative?

The README says npm install and package-lock.json is present, so the migration regenerates that lockfile and leaves the divergent yarn.lock untouched, with the divergence noted in the description for you to clean up separately.

### How do I check the result later?

Save the execution_id the API returns and read the session when you are back, or receive the key's completion webhook. The pull request description carries the before and after counts; the transcript holds the runs themselves.
