Skip to content

fix(jest-reporters): do not check coverageThreshold when running a shard - #16372

Open
Aeirx wants to merge 3 commits into
jestjs:mainfrom
Aeirx:fix/coverage-threshold-with-shard
Open

fix(jest-reporters): do not check coverageThreshold when running a shard#16372
Aeirx wants to merge 3 commits into
jestjs:mainfrom
Aeirx:fix/coverage-threshold-with-shard

Conversation

@Aeirx

@Aeirx Aeirx commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Refs #12751.

A shard runs a subset of the test files, but coverage is still reported for every file matching collectCoverageFrom — the ones the shard did not exercise come out at zero. _checkThreshold then runs against that mix, so a global threshold fails on every shard however well the project is actually covered.

Reproduced on 29.7.0 and on 30.4.2. Four sources, each fully covered by its own test file, global thresholds at 90%:

$ npx jest                       # full run
All files |     100 |      100 |     100 |     100 |
Tests:       4 passed, 4 total          # exit 0

$ npx jest --shard=1/4
 mod1.js  |       0 |        0 |       0 |       0 |
 mod2.js  |       0 |        0 |       0 |       0 |
 mod3.js  |       0 |        0 |       0 |       0 |
 mod4.js  |     100 |      100 |     100 |     100 |
All files |      25 |       25 |      25 |      25 |
Jest: Coverage for statements (25%) does not meet "global" threshold (90%)
Jest: Coverage for branches (25%) does not meet "global" threshold (90%)
Jest: Coverage for lines (25%) does not meet "global" threshold (90%)
Jest: Coverage for functions (25%) does not meet "global" threshold (90%)
Tests:       1 passed, 1 total          # exit 1

Every test that ran passed. The failure is entirely the three files this shard was never going to run.

The change

Skip the threshold check for a sharded run and say so, rather than reporting a verdict on coverage the run never set out to collect:

Jest: Coverage thresholds are not checked when running a shard, as shard 1/4
only covers part of the project. Merge the coverage reports from all shards
and check the thresholds against the merged report.

Skipped rather than adjusted because there is no adjustment to make: what a global threshold describes is the whole project's coverage, and no single shard has it.

--shard=1/1 keeps its thresholds. It runs every test file, so it is a complete run. The condition is shardCount > 1, not "shard is set" — getting that wrong would quietly stop enforcing coverage for anyone who shards by one.

No new option and no merge mode. @SimenB wrote in 2022 that multiple runs are the only way to make the threshold check work and that he was unsure Jest wants coverage merging as a mode; this follows from that rather than arguing with it. Collecting per shard and merging is already the documented route to a whole-project number, and the warning points at it.

A decision worth your call

The skip covers all threshold groups, not only global. A path or glob threshold on a file the shard did cover would still be meaningful, so there is a narrower fix available.

I did not take it because Jest cannot tell which files a given shard should cover — the split is by test file, so any group can contain files at zero purely because their tests landed in another shard. Skipping everything is predictable; skipping some groups but not others depends on where the sequencer happened to put things. Happy to narrow it to global if you would rather.

Tests

Three e2e cases in coverageThreshold.test.ts:

  • does not check the threshold when running one shard of several — the fix. Fails without it (exit 1 instead of 0); I checked by neutralising the guard and re-running.
  • checks the threshold when the run is a single shard--shard=1/1 is unaffected.
  • checks the threshold on a single shard that misses it — a complete run genuinely short of the threshold still fails, so the skip is about sharding and not a way to switch thresholds off.

The last two pass with or without the change by design; they are there to pin the boundary, not to detect the bug.

check result
packages/jest-reporters + coverage/shard/summary e2e 122 tests, 74 snapshots pass
all e2e/__tests__/coverage* 36 tests, 40 snapshots pass
tsc --noEmit on jest-reporters 0 errors
ESLint + Prettier on changed files clean

Docs updated on both sides — coverageThreshold in Configuration.md and --shard in CLI.md — since the two are only surprising together. CHANGELOG entry under Fixes.

I have not run the full suite; the above is the coverage, reporter and shard surface plus the gates.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 19, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: Aeirx / name: Ayush Sharma (5d0e5fb)

@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit a95ff26
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6a86ed4b7411d600087d8b42
😎 Deploy Preview https://deploy-preview-16372--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the require-changelog If a PR does requires a changelog entry label Aug 19, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16372

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16372

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16372

create-jest

npm i https://pkg.pr.new/create-jest@16372

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16372

expect

npm i https://pkg.pr.new/expect@16372

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16372

jest

npm i https://pkg.pr.new/jest@16372

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16372

jest-circus

npm i https://pkg.pr.new/jest-circus@16372

jest-cli

npm i https://pkg.pr.new/jest-cli@16372

jest-config

npm i https://pkg.pr.new/jest-config@16372

@jest/console

npm i https://pkg.pr.new/@jest/console@16372

@jest/core

npm i https://pkg.pr.new/@jest/core@16372

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16372

jest-diff

npm i https://pkg.pr.new/jest-diff@16372

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16372

jest-each

npm i https://pkg.pr.new/jest-each@16372

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16372

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16372

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16372

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16372

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16372

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16372

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16372

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16372

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16372

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16372

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16372

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16372

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16372

jest-mock

npm i https://pkg.pr.new/jest-mock@16372

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16372

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16372

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16372

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16372

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16372

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16372

jest-runner

npm i https://pkg.pr.new/jest-runner@16372

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16372

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16372

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16372

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16372

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16372

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16372

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16372

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16372

@jest/types

npm i https://pkg.pr.new/@jest/types@16372

jest-util

npm i https://pkg.pr.new/jest-util@16372

jest-validate

npm i https://pkg.pr.new/jest-validate@16372

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16372

jest-worker

npm i https://pkg.pr.new/jest-worker@16372

pretty-format

npm i https://pkg.pr.new/pretty-format@16372

commit: a95ff26

Aeirx added 2 commits August 19, 2026 15:00
A shard runs a subset of the test files, but coverage is still reported for
every file matching collectCoverageFrom -- the ones the shard did not
exercise come out at zero. The threshold is then checked against that mix,
so a `global` threshold fails on every shard no matter how well the project
is actually covered.

Reproduced on 29.7.0 and on 30.4.2: four test files, four sources, each
fully covered by its own test, `global` thresholds at 90%. A full run
reports 100% and passes. `--shard=1/4` reports 25% and fails all four
thresholds, while the one test file it ran passes.

Skip the check for a sharded run and say so, rather than reporting a
verdict on coverage the run never set out to collect. Skipped rather than
adjusted because there is no adjustment to make: what a `global` threshold
describes is the whole project's coverage, and no single shard has it.
Enforcing it belongs after the shards' coverage is merged.

`--shard=1/1` runs every test file, so it is a complete run and keeps its
thresholds -- the condition is shardCount > 1, not "shard is set".

No new option and no merge mode, since collecting coverage per shard and
merging it is already the documented way to get a whole-project number.

Refs jestjs#12751
The e2e cases run Jest in a subprocess, so the new branch showed as
uncovered on the patch. Exercise it directly through CoverageReporter as
well: a run with shardCount 4 records no error and logs the warning, and
one with shardCount 1 still reports the threshold error.

Also point the changelog entry at this PR rather than the issue, which is
what scripts/checkChangelog.mjs requires.
@Aeirx
Aeirx force-pushed the fix/coverage-threshold-with-shard branch from 70b184d to 47f5975 Compare August 19, 2026 09:30

@soltonigiri soltonigiri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clarify that Jest does not currently provide a built-in command for applying coverageThreshold to a merged coverage file. I could merge the shard coverage-final.json files, but reproducing Jest's exact-file/directory/glob groups and negative-threshold semantics required a custom script that manually drove CoverageReporter. The docs should either show a concrete external-service/custom-script workflow and its semantic limitations, or avoid implying that this can be completed with Jest itself.

…is not Jest's to do

The previous wording told the reader to merge the shards' coverage and
check the thresholds against the merged report, which reads as though Jest
has a step for that. It does not: there is no flag that merges coverage
across runs, and none that checks a threshold against a report Jest did not
just produce.

Say so, name the workflow that does work -- `json` reporter per shard,
merge the `coverage-final.json` files, enforce with a coverage service or
`nyc check-coverage` -- and name what it costs. `nyc check-coverage` takes
global and per-file thresholds, so this option's glob and path groups do not
carry over, and neither does a negative threshold's "maximum uncovered
items" rule. A config using those has no exact equivalent outside Jest.

The runtime warning made the same implication and now says the enforcement
happens outside Jest.

Reported by @soltonigiri in review, who hit exactly this: merging the
coverage files was straightforward, reproducing the threshold semantics was
not.
@Aeirx

Aeirx commented Aug 20, 2026

Copy link
Copy Markdown
Author

You are right, and the wording was doing exactly what you describe. Fixed in a95ff26.

I checked the claim rather than taking it on faith — the coverage flags are collectCoverage, collectCoverageFrom, coverage, coverageDirectory, coveragePathIgnorePatterns, coverageProvider, coverageReporters, coverageThreshold. Nothing merges coverage across runs, and nothing checks a threshold against a report Jest did not just produce. "Merge the reports and check the thresholds against the merged report" was a sentence that reads like an instruction and is not one.

The docs now say that outright, name the workflow that does work, and name what it costs:

Jest has no built-in step for enforcing thresholds across shards: it cannot merge the shards' coverage, and it cannot check a threshold against a coverage file it did not just produce. Enforcing a whole-project number therefore happens outside Jest — have each shard write the json reporter, merge the resulting coverage-final.json files (istanbul-lib-coverage, nyc merge), and check the merged report with your coverage service or nyc check-coverage.

Note what that does not carry over. nyc check-coverage takes global and per-file thresholds; it does not reproduce this option's glob and path groups, and it has no equivalent of a negative threshold's "maximum uncovered items" rule. A coverageThreshold using those features has no exact equivalent outside Jest, and reproducing it means driving the reporter yourself.

That last paragraph is your finding specifically — the part you hit was not the merging, it was that the group and negative-threshold semantics have no equivalent on the other side. Worth stating in the docs rather than leaving each person to discover it with a custom script.

The runtime warning made the same implication and now ends with "enforce a whole-project threshold outside Jest, against the merged coverage from every shard" instead of implying Jest will do it.

Two things I deliberately did not do, both of which I think are the maintainers' call rather than mine:

  • Ship a merge command. @SimenB said in 2022 he was unsure Jest wants coverage merging as a mode, so this PR stays a skip plus honest docs. If the conclusion is that the gap you found should be closed in Jest rather than documented, that is a larger change and a separate one.
  • Document a specific external service. I named nyc check-coverage because it is the closest general-purpose equivalent and its limits are checkable; naming a vendor felt like the wrong thing to put in Jest's docs.

Tests, lint and format re-run after the change: 26 passing across the unit and e2e suites.

@soltonigiri soltonigiri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the documentation concern. I re-reviewed a95ff26 and verified with the built CLI that threshold checking is skipped only when shardCount > 1. Unsharded runs and --shard=1/1 still enforce global, path/directory, glob, positive-percentage, and negative-uncovered thresholds.

Multi-shard runs still produce text/JSON coverage and emit one warning, including under Circus, Jasmine, and a multi-project configuration. I also verified that the two shard coverage-final.json files merge to the complete aggregate.

The revised docs now accurately state that Jest cannot merge or re-check the reports itself, and they call out the path/glob and negative-threshold semantics that nyc check-coverage cannot reproduce. My earlier concern is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

require-changelog If a PR does requires a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants