Skip to main content

ADR-019: Migrate from Jest to Vitest for Testing

Statusโ€‹

Accepted

Dateโ€‹

2024-12-16

Contextโ€‹

Our test suite has 2,585 tests across 96 test files. We're experiencing significant issues with Jest's ESM mocking capabilities:

Current Problemsโ€‹

  1. 59 test failures in CI - Tests timeout in GitHub Actions that pass locally
  2. jest.unstable_mockModule() limitations - The ESM mocking API is experimental and unreliable:
    • Requires async top-level imports (await import() after mock setup)
    • Mocks don't hoist like CommonJS jest.mock()
    • TypeScript type inference struggles with mocked types
    • Still marked "unstable" even in Jest 30
  3. Slow test execution - ESM transformation overhead adds ~2-3x execution time
  4. Complex workarounds - Tests need explicit --experimental-vm-modules flag and special patterns

Failing Test Files (All Timeout Issues)โ€‹

FileDurationFailures
deployment-guidance-tool.test.ts210s12 tests
troubleshoot-guided-workflow-tool.test.ts377s15 tests
review-existing-adrs-tool.test.ts120s10 tests
adr-bootstrap-validation-tool.test.ts241s22 tests

Alternatives Consideredโ€‹

  1. Stay with Jest + Increase Timeouts: Doesn't solve the underlying ESM mocking issues
  2. Jest + Sinon: Better mocking but doesn't solve ESM transformation overhead
  3. Jest + Dependency Injection: High refactoring effort, same speed issues
  4. Vitest: Native ESM support, faster execution, Jest-compatible API

Decisionโ€‹

Migrate from Jest to Vitest for all testing.

Why Vitestโ€‹

  1. Native ESM Support: No experimental flags, no transformation overhead
  2. 3-5x Faster: Leverages Vite's optimized module handling
  3. 95% Jest Compatible: Same describe/it/expect syntax, similar mocking API
  4. Better TypeScript: Native support without Babel
  5. Simpler Mocking: vi.mock() works like jest.mock() should have worked

Migration Syntax Changesโ€‹

// Before (Jest ESM)
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../src/utils/adr-discovery.js', () => ({
discoverAdrsInDirectory: jest.fn(),
}));
const { generateDeploymentGuidance } = await import('../../src/tools/...');

// After (Vitest)
import { vi, describe, it, expect } from 'vitest';
vi.mock('../../src/utils/adr-discovery.js', () => ({
discoverAdrsInDirectory: vi.fn(),
}));
import { generateDeploymentGuidance } from '../../src/tools/...'; // Normal import!

Migration Planโ€‹

Phase 1: Setup (This PR)โ€‹

  • Install Vitest and dependencies
  • Create vitest.config.ts
  • Update package.json scripts
  • Migrate one test file as proof-of-concept

Phase 2: Core Migrationโ€‹

  • Migrate failing test files first (4 files, ~59 tests) โ€” deliberately left unchecked. See Corrections.
  • Migrate tool tests (tests/tools/)
  • Migrate utility tests (tests/utils/)

Phase 3: Complete Migrationโ€‹

  • Migrate integration tests
  • Migrate performance tests
  • Remove Jest dependencies
  • Update CI workflows

Phase 4: Cleanupโ€‹

  • Remove Jest configuration
  • Update documentation
  • Archive migration scripts

Correctionsโ€‹

2026-08-27 (#1462) โ€” this ADR under-reported itself by twelve boxes.

ADR-019 is Accepted and every migration box was unchecked, while the migration had in fact completed. Verified individually before ticking, not assumed from the status:

boxevidence
Vitest installeddevDependencies.vitest present
vitest.config.tsfile exists
package.json scriptstest is vitest run
proof-of-concept, tool, utility, integration, performance testsall present under tests/
Jest dependencies removedno jest / ts-jest / @types/jest in package.json
CI workflows updatedno jest in .github/workflows/*.yml
Jest configuration removedno jest.config.*
migration scripts archivedscripts/migrate-to-vitest.sh removed in this change โ€” 73 lines, referenced nowhere, retirement.py returned RETIREMENT_REVIEW; git preserves it

Zero files anywhere in src/ or tests/ still import from Jest.

The one box left unchecked, and whyโ€‹

"Migrate failing test files first (4 files, ~59 tests)." This is not verifiable as written and is not this ADR's to close.

The framework migration is complete, but 70 tests are skipped across 14 files. #1477 measured 13 of them sharing one root cause: ESM mocking of ResearchOrchestrator, which is also the ADR-018a violation in #1461. So the remaining work is not "finish migrating to Vitest" โ€” it is "make ESM mocking work in this codebase", which is #751's scope, gated on #1461.

Ticking it would claim a completion that 70 skipped tests contradict. Leaving it unchecked with the reason stated is the honest record; #751 and #1461 carry the work.

Consequencesโ€‹

Positiveโ€‹

  • Faster test execution (3-5x improvement expected)
  • Reliable ESM mocking without experimental flags
  • Simpler test setup code
  • Better TypeScript integration
  • More active development and community support

Negativeโ€‹

  • One-time migration effort (~2-4 hours)
  • Team needs to learn minor API differences
  • Some Jest-specific features may need alternatives

Neutralโ€‹

  • Test files need syntax updates (mostly search-replace)
  • CI configuration needs updates
  • Coverage reporting tools remain compatible

Referencesโ€‹