Vitest "coverage.thresholds" Not Met - Coverage Gate Fails CI
Tests passed, but Vitest exited non-zero because coverage.thresholds was not met. Either new code is untested, or coverage.all: true is counting files that no test imports as 0% and dragging the totals down.
What this error means
CI fails after a green run with "ERROR: Coverage for lines (78%) does not meet global threshold (80%)." The suite itself passes - the failure is the coverage gate, not an assertion.
ERROR: Coverage for lines (78.4%) does not meet global threshold (80%)
ERROR: Coverage for branches (71.2%) does not meet global threshold (75%)Common causes
New code added without tests
A feature lowered the overall percentage below the configured thresholds. Global thresholds compare totals, so well-covered files cannot offset a large untested addition.
coverage.all counts unimported files as 0%
With coverage.all: true, Vitest instruments every source file matching the include glob - even ones no test imports - counting them as 0% and sinking the average.
How to fix it
Scope coverage and set realistic thresholds
Exclude non-testable files and use per-glob thresholds so unrelated areas are not held to the same bar.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.stories.tsx', 'src/types/**'],
thresholds: { lines: 80, branches: 75, 'src/legacy/**': { lines: 40 } },
},
},
});See exactly what is uncovered
- Run
vitest run --coverageand open the text/HTML report. - Exclude generated, type-only, or story files from
coverage.include. - Add tests for the specific uncovered lines the report lists.
How to prevent it
- Exclude non-testable files from coverage
include. - Use per-path thresholds for legacy areas.
- Review coverage in PRs to catch regressions before merge.