Skip to content
rjest

← Back to writing

Drop-in compatibility: the contract that makes adoption tolerable

rjest Team · ·
compatibilityengineering

The pitch for a new test runner usually goes “ours is faster.” The honest sequel is “and here is the migration.” That sequel kills most adoptions before they begin, because migrating tests is the kind of work that has no business value and a lot of risk. If your existing suite passes today and the new runner says it does not, you have to figure out which one is wrong, and that work is rarely glamorous.

rjest was designed to avoid that conversation. The pitch is not “ours is faster.” The pitch is “ours is faster and you do not have to migrate anything.” The second half is doing most of the work.

This post is about the contract that makes the second half possible.

What “drop-in” means in practice

For a Jest replacement, “drop-in” decomposes into four pledges:

  1. Read the same config files. No new format, no migration script.
  2. Accept the same CLI flags. Your CI scripts continue to work.
  3. Run the same test syntax. Your tests are not patched.
  4. Produce the same output. Your humans and your machines continue to parse it.

We treat each as a hard contract. A deviation is not a “tradeoff we accept for performance.” It is an issue we fix.

Config files

rjest reads jest.config.js, jest.config.ts, jest.config.mjs, and the jest block in package.json. The supported keys are the keys Jest supports — testEnvironment, setupFiles, setupFilesAfterEach, testMatch, testPathIgnorePatterns, moduleNameMapper, transform, globalSetup, globalTeardown, and so on.

We do not require you to remove anything. If a config key references a transformer that rjest’s daemon does not natively run (e.g., a custom Babel preset that does something exotic), rjest’s compatibility matrix documents the behavior: either it falls back to Babel for that subset, or it warns clearly. Silent disagreement is the worst possible outcome and we treat it as a bug.

CLI flags

The CLI surface includes the flags people actually script against: --watch, --coverage, --runInBand, --testNamePattern, --onlyChanged, --ci, --config, --projects, --silent, --verbose, --listTests, --json. We added two: --machine for AI-agent-friendly structured output, and --daemon-status / --daemon-stop / --daemon-restart for daemon lifecycle.

The contract is that an existing package.json script that says "test": "jest --coverage --ci" should keep working when you change the binary name. If a flag you depend on is not yet supported, that is a tracked issue, not a feature request.

Test syntax

This is the easiest contract to honor, because Jest’s test syntax is a JavaScript API that runs inside your test process. We do not patch your code; we run it in a Node.js worker that exposes the same describe, it, test, expect, beforeEach, afterEach, jest.fn(), jest.mock(), jest.spyOn(), jest.useFakeTimers(), snapshots, and so on.

If you have used Jest features that depend on internal APIs not on the documented matchers list — say, custom snapshot serializers or unconventional matcher composition — the compatibility matrix is the canonical place to check. We track every divergence we know about.

Output

This one is subtle. Jest’s text output is read by humans, but also by editors (IDE test runners), CI parsers, and increasingly by AI coding agents that watch test output to decide what to fix next. If we say “PASS” where Jest says “PASS,” but in a different shape, every one of those downstream parsers breaks.

So rjest’s text reporter matches Jest’s format. Default summary, error blocks, snapshot diffs, watch-mode menu — all aligned. The --json output matches Jest’s --json schema, including failure structures. --machine is an additive format we designed for agents; it does not replace anything.

Why the contract has teeth

A compatibility claim is only useful if you can rely on it. We do three things to make that reliance reasonable:

A growing compatibility matrix. The repo’s compatibility doc tracks every Jest behavior we claim to support, what state it is in, and what known divergences exist. New issues get triaged into this matrix.

Conformance tests. A subset of Jest’s own test corpus runs against rjest in our CI. Failures are visible; they do not become permanent unless we explicitly mark them as out-of-scope (which is rare and requires justification).

Side-by-side as the first-class workflow. The first step of evaluating rjest in your repo is supposed to be “run it alongside Jest in CI and compare.” We document this pattern because we want disagreements caught early, when they are easy to fix on our side.

The cost of the contract

Honoring the contract has a cost, and that cost shapes the project.

It limits how aggressively we can pre-compile or rewrite test code. Some “free” speedups — like dropping support for dynamic jest.mock() factories that close over local variables — are not free, because real codebases use them. We do not take those shortcuts.

It rules out a “rjest 2.0 with a slightly different config.” If we ever fork the config schema, we have broken the only feature that matters for adoption. We will not.

It commits us to maintaining behaviors that are arguably bad ideas in the original. Jest has a few corners — particular interactions between mocks and module caching, particular behaviors around fake timers — that are confusing. We match them, including the confusing parts, because changing them silently is worse.

The benefit is large. The first hour with rjest in a real repo is uneventful, which is the highest praise you can give a test runner during evaluation.

What we ask in return

When a Jest behavior diverges in rjest, we want to know. The reproducer can be small. The triage on our side is fast because the contract is unambiguous: if Jest does X and rjest does Y, Y is the bug.

We also ask, gently: do not silently work around a divergence. Workarounds make the matrix invisible. File the issue first. Workaround second.

The shape of the issue tracker

You can audit the discipline by looking at how we triage bugs. A divergence report is labeled compat. Those issues have a higher priority than perf issues by default, because performance can be improved iteratively while compatibility either is or is not. If a compat issue is open and unresolved, it appears on the compatibility matrix as a known divergence — so users can make informed decisions even before we ship the fix.

This sounds bureaucratic, but it changes what we do. When we are tempted to take a shortcut that would speed up a transform but make a mock behave subtly differently, the compat discipline pulls us back. The shortcut is not a shortcut; it is a defect we have not yet paid for.

Things compatibility does not mean

A short list, because they come up:

  • Compatibility does not mean “rjest knows about every Jest plugin.” Some plugins do things in the Node-side Jest runtime that we cannot intercept from the daemon. For those, the compatibility matrix is honest about what we can and cannot run.
  • Compatibility does not mean “your existing watch-mode keystrokes work.” Watch-mode UI is a moving target across Jest versions; we match the most common workflow and document any deltas.
  • Compatibility does not mean “if you typed jest --foo, rjest also accepts --foo.” The CLI surface we honor is the one documented in the repo. Undocumented flags are unsupported. If you depended on one, please file an issue — that is how the documented surface grows.

The summary

A faster test runner is a research project. A faster test runner that runs your existing tests unchanged is an infrastructure choice. rjest is in the second category by design, and the compatibility contract is the part of the design that makes it so.

The performance work — daemon, SWC, sled, warm workers — is what we are proud of technically. The compatibility work is what makes that performance reachable in your repository this week, not next quarter. Compatibility is the cheaper-feeling half of the project to talk about. It is the half that takes more discipline to ship.