Atlas
TechnicalFrom Atlas

Run Only Affected Tests in a Monorepo with AI Agents

Cut test time when AI agents change a monorepo: run only affected tests with pnpm filters and Vitest, plus the safety rules that keep narrowing honest.

The short version

  • Running only the tests a change can reach shortens each check, which matters most when several agents land work in parallel and every finished branch wants a run.
  • In a pnpm monorepo, the filter "...[origin/main]" selects changed packages and everything that depends on them.
  • In a single root Vitest run, vitest related and the --changed option narrow by files, and --project narrows by package.
  • Narrowing is only safe with fallback rules: run everything when root files, shared config or lockfiles change.
  • Keep one full run before release, so narrowed runs speed up the loop without becoming the only proof.
On this page
  1. The short version
  2. Why full test runs hurt more with agents
  3. What does "affected tests" mean?
  4. Option 1: filter by package with pnpm
  5. Option 2: one root Vitest run
  6. When must you run everything?
  7. Where do the agents fit in?
  8. How Atlas handles this
  9. Conclusion
  10. Questions people ask

If AI coding agents change your monorepo, you do not need to run the whole test suite after every change. Run the tests of the packages the change touches plus every package that depends on them, and fall back to the full suite whenever something outside the packages changed. This article shows how to do that with pnpm and Vitest, and where it goes wrong.

Why full test runs hurt more with agents

A human engineer runs the full suite a few times a day. A team of agents works in parallel, and each finished branch wants a check. If the suite takes 15 minutes, ten batches cost two and a half hours of machine time, and the agents wait on it.

There is a second problem. When the whole suite runs as one root command, such as a single vitest run at the monorepo root, it looks like one opaque step. Tooling that narrows per package never sees inside it, so teams often settle for "run everything every time".

What does "affected tests" mean?

A test is affected by a change if it can reach the changed code through imports. In a monorepo that has two layers:

  1. Package level. A package changed, or a package it depends on changed.
  2. File level. Inside one run, a test file imports (directly or through other modules) a file that changed.

Both layers are static analysis. They are good, not perfect, and the last section covers the gaps.

Option 1: filter by package with pnpm

If each package has its own test script, pnpm can select packages by git changes. The [origin/main] selector picks packages changed since that branch. A ... prefix adds dependents, and a ... suffix adds dependencies.

bash
# changed packages only
pnpm --filter "[origin/main]" test

# changed packages plus the packages that depend on them
pnpm --filter "...[origin/main]" test

For testing, you almost always want the second form. If an agent edits packages/core, the tests of packages/api, which imports it, can break even though api is untouched.

Details from the pnpm docs worth knowing:

  • Since pnpm 12.7.0 the comparison uses the merge base, the commit where your branch forked from origin/main. Commits that landed on main after you branched are not counted as your changes.
  • Uncommitted changes are included, so this works for an agent's working tree before it commits.
  • In a shallow clone that lacks the merge base, pnpm compares against the ref directly. Fetch enough history in CI.
  • The selector needs Git 2.24 or newer.
  • --changed-files-ignore-pattern ignores files such as READMEs when deciding what changed:
bash
pnpm --filter="...[origin/main]" --changed-files-ignore-pattern="**/README.md" run test

Option 2: one root Vitest run

Some repos run Vitest once from the root, with packages defined as projects:

ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    projects: ['packages/*'],
  },
})

Here pnpm --filter does not help, because there is no per-package script to select. Vitest has its own tools, and you can combine them.

Narrow by file

vitest related runs only the tests that cover the files you list:

bash
vitest related --run packages/core/src/price.ts packages/core/src/tax.ts

Watch mode is on by default, so --run is needed in scripts. Vitest follows static imports. It does not follow dynamic imports whose path is a variable, such as import(filepath).

The --changed option does the same job from git. Its since value, a commit or branch, is optional:

bash
vitest run --changed origin/main

Narrow by project

--project selects projects by name. It can be repeated and supports wildcards and ! exclusions:

bash
vitest run --project core --project api
vitest run --project '!e2e'

For projects defined by a glob, the name comes from the name field in the nearest package.json, or the folder name if there is none. Names must be unique, or Vitest throws an error.

Glue it together

To get "changed packages plus dependents" in a root run, ask pnpm which packages are affected and pass them to Vitest as projects. A sketch, assuming project names equal package names:

bash
#!/usr/bin/env bash
set -euo pipefail

# Names of changed packages and their dependents
names=$(pnpm --filter "...[origin/main]" exec node -p "require('./package.json').name")

args=()
for n in $names; do args+=(--project "$n"); done

vitest run "${args[@]}"

Treat this as a starting point, not a drop-in. Test it on a throwaway branch before you trust it, and check that your package names really match your project names.

When must you run everything?

Narrowing is a bet that the graph tells the truth. Make the bet only where it is safe. Run the full suite when:

  • A file outside every package changed: root config, shared scripts, CI files, the lockfile.
  • A test lives at the repository root, outside any package.
  • The command is already aimed at specific files. Do not narrow twice.
  • The selector returns nothing and you cannot explain why. An empty selection that silently passes is the worst outcome.

Also keep tests that sit outside the packages in the narrowed run. Integration and end-to-end folders usually import the packages indirectly or over the network, so the import graph cannot reach them.

What narrowing cannot see

  • Dynamic imports with computed paths, as above.
  • Behaviour through the database, queues, environment variables or files on disk.
  • Generated code. If a generated file is not part of the package's imports, a change to its source may not select the right tests.
  • Snapshot and fixture files that tests read at runtime.

Where do the agents fit in?

Tell the agent the narrow command and the full command, and when each applies. A short block in your project's instruction file is enough:

markdown
## Testing
- While working: `pnpm --filter "...[origin/main]" test`
- Before saying a task is done, or if root files, config or the lockfile changed: `pnpm verify`
- Never delete or skip a test to make a run pass.

Here pnpm verify stands for whatever script runs your full suite. Keep the full run in CI on main and before every release. Narrowed runs speed up the loop. They should not be the only evidence that the whole product works.

How Atlas handles this

Atlas is a team of AI engineers built on Claude Code. Its landing train merges up to six finished branches and runs the project's own checks once on the result. If that batch is red, it is split until the branch that broke it is found. How work is tested before landing, whether only what changed or the full suite, is the operator's choice on the Tests page.

Atlas does not yet narrow a single whole-monorepo Vitest run to the packages a change can reach. That is coming to Atlas, built in, so it will need no setup. Until then, you can set the Tests page to the choice that suits your repo, and give your engineers the narrow and full commands from the section above. Read more at Atlas.

Conclusion

Start with pnpm --filter "...[origin/main]" test if your packages have their own test scripts. If you run Vitest once from the root, combine --project with the changed-package list, and add explicit fallbacks to the full suite. Write both commands into your instruction file so the agents use them the same way each time, and keep one full run as the gate before release.

Questions people ask

How do I run only affected tests in a pnpm monorepo?

Use pnpm --filter "...[origin/main]" test. It selects packages changed since origin/main plus the packages that depend on them, and runs their test script.

What does vitest related do?

It runs only the tests that cover the source files you list, for example vitest related --run src/a.ts. It follows static imports but not dynamic imports with a variable path.

Is it safe to skip tests that a change does not reach?

It is safe only with fallbacks. Run the full suite when files outside the packages change, when a test sits at the root, or when the selection is empty, and keep a full run before release.

Can Vitest run only one package of a monorepo?

Yes. If packages are defined as projects, use vitest run --project <name>. The flag can be repeated and supports wildcards and ! exclusions.

Sources

  1. Vitest CLI reference
  2. pnpm: Filtering
  3. Vitest: Test Projects

Researched and drafted with Claude, then checked against the sources above. Atlas is an independent product, not affiliated with Anthropic or OpenAI. Claude and Claude Code are trademarks of Anthropic; Codex is a product of OpenAI.