JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 388
  • Score
    100M100P100Q86599F
  • License MIT

A fast, targeted mutation testing framework for JavaScript and TypeScript

Package Exports

  • @mutineerjs/mutineer

Readme

                  _   _
                 | | (_)
  _ __ ___  _   _| |_ _ _ __   ___  ___ _ __
 | '_ ` _ \| | | | __| | '_ \ / _ \/ _ \ '__|
 | | | | | | |_| | |_| | | | |  __/  __/ |
 |_| |_| |_|\__,_|\__|_|_| |_|\___|\___|_|

       === ~> !== · && ~> || · + ~> -

Mutineer is a fast, targeted mutation testing framework for JavaScript and TypeScript. Mutineer introduces small code changes (mutations) into your source files and runs your existing tests to see if they catch the defect. If a test fails, the mutant is "killed" - meaning your tests are doing their job. If all tests pass, the mutant "escaped" - revealing a gap in your test coverage.

Built for Vitest with first-class Jest support. Other test runners can be added via the adapter interface.

How It Works

  1. Baseline - runs your test suite to make sure everything passes before mutating
  2. Mutate - applies AST-safe operator replacements to your source files (not your tests)
  3. Test - re-runs only the tests that import the mutated file; compile errors are detected via parallel TypeScript workers
  4. Report - prints a summary with kill rate, escaped mutants, and per-file breakdowns

Mutations are applied using Babel AST analysis, so operators inside strings and comments are never touched. Mutated code is written to a temporary __mutineer__ directory next to each source file, then loaded at runtime via Vite plugins (Vitest) or custom resolvers (Jest).

Supported Mutations (WIP)

Category Mutator Transformation
Equality flipStrictEQ ===!==
flipStrictNEQ !=====
flipEQ ==!=
flipNEQ !===
Boundary relaxLE <=<
relaxGE >=>
tightenLT <<=
tightenGT >>=
Logical andToOr &&||
orToAnd ||&&
nullishToOr ??||
Arithmetic addToSub +-
subToAdd -+
mulToDiv */
divToMul /*
modToMul %*
powerToMul ***
Return value returnToNull return xreturn null
returnToUndefined return xreturn undefined
returnFlipBool return truereturn false
returnZero return nreturn 0
returnEmptyStr return sreturn ''
returnEmptyArr return [...]return []

Installation

npm i @mutineerjs/mutineer

Usage

Commands

Command Description
mutineer init Create a mutineer.config.ts with minimal defaults
mutineer run Run mutation testing
mutineer clean Remove leftover __mutineer__ temp directories

Quick Start

Try it immediately with npx:

npx @mutineerjs/mutineer init
npx @mutineerjs/mutineer run

Or add scripts to your package.json (recommended for team projects):

{
  "scripts": {
    "mutineer": "mutineer run",
    "mutineer:init": "mutineer init"
  }
}
npm run mutineer:init
npm run mutineer

CLI Options (for mutineer run)

Flag Description Default
--runner <type> Test runner: vitest or jest vitest
--config, -c Path to config file auto-detected
--concurrency <n> Parallel workers (min 1) CPUs - 1
--changed Only mutate files changed vs base branch --
--changed-with-imports Include local imports of changed files --
--full Mutate full codebase, skipping confirmation prompt --
--only-covered-lines Skip mutations on uncovered lines --
--per-test-coverage Run only tests that cover the mutated line --
--coverage-file <path> Path to Istanbul coverage JSON auto-detected
--min-kill-percent <n> Fail if kill rate is below threshold --
--progress <mode> Display mode: bar, list, or quiet bar
--timeout <ms> Per-mutant test timeout 30000
--report <format> Output format: text or json (writes mutineer-report.json) text
--shard <n>/<total> Run a slice of mutants (e.g. --shard 1/4) --
--skip-baseline Skip the baseline test run --
--vitest-project <name> Filter mutations to a specific Vitest workspace project (requires test.projects) --
--typescript Enable TypeScript type-check pre-filtering (skips mutants that cause compile errors) auto
--no-typescript Disable TypeScript type-check pre-filtering --

Examples

Run mutations on only the files you changed:

npm run mutineer -- --changed

Run with Jest and a minimum kill rate:

npm run mutineer -- --runner jest --min-kill-percent 80

Focus on covered code with 2 parallel workers:

npm run mutineer -- --only-covered-lines --concurrency 2

Configuration

Create a mutineer.config.ts (or .js / .mjs) in your project root with mutineer init, or manually:

import { defineMutineerConfig } from 'mutineer'

export default defineMutineerConfig({
  source: 'src',
  runner: 'vitest',
  vitestConfig: 'vitest.config.ts',
  minKillPercent: 80,
  onlyCoveredLines: true,
})

Config Options

Option Type Description
source string | string[] Glob patterns for source files to mutate
targets MutateTarget[] Explicit list of files to mutate
runner 'vitest' | 'jest' Test runner to use
vitestConfig string Path to vitest config
jestConfig string Path to jest config
include string[] Only run these mutators
exclude string[] Skip these mutators
excludePaths string[] Glob patterns for paths to skip
maxMutantsPerFile number Cap mutations per file
minKillPercent number Fail if kill rate is below this
onlyCoveredLines boolean Only mutate lines covered by tests
perTestCoverage boolean Use per-test coverage to select tests
baseRef string Git ref for --changed (default: origin/main)
testPatterns string[] Globs for test file discovery
extensions string[] File extensions to consider
vitestProject string | string[] Filter to a specific Vitest workspace project
typescript boolean | { tsconfig?: string } Enable TS type-check pre-filtering; auto-detected if tsconfig.json present

Large repos can generate thousands of mutations. These strategies keep runs fast and incremental.

When you run mutineer run without --changed, --changed-with-imports, or --full on an interactive terminal, mutineer warns you and lets you narrow scope before starting:

Warning: Running on the full codebase may take a while.

  [1] Continue (full codebase)
  [2] --changed          (git-changed files only)
  [3] --changed-with-imports (changed + their local imports)
  [4] Abort

1. PR-scoped runs (CI) — --changed-with-imports

Run only on files changed in the branch plus their local imports:

mutineer run --changed-with-imports
  • Tune the import resolution depth with importDepth in config (default: 1)
  • Add --per-test-coverage to only run tests that cover the mutated line
  • Recommended package.json script:
"mutineer:ci": "mutineer run --changed-with-imports --per-test-coverage"

2. Split configs by domain

Create a mutineer.config.ts per domain and run selectively:

mutineer run -c src/api/mutineer.config.ts

Each config sets its own source glob and minKillPercent. Good for monorepos or large modular projects — domains can also be parallelized in CI.

3. Combine filters to reduce scope

  • --only-covered-lines — skips lines not covered by any test (requires a coverage file)
  • maxMutantsPerFile — caps mutations per file as a safety valve
  • Combine for maximum focus:
mutineer run --changed-with-imports --only-covered-lines --per-test-coverage

File Support

  • TypeScript and JavaScript modules (.ts, .js, .tsx, .jsx)
  • Vue Single File Components (.vue with <script setup>)

Extending: Adding a New Test Runner

Mutineer uses an adapter pattern to support different test runners. To add a new one, implement the TestRunnerAdapter interface:

import type {
  TestRunnerAdapter,
  TestRunnerAdapterOptions,
  BaselineOptions,
  MutantPayload,
  MutantRunResult,
} from 'mutineer'

export function createMyRunnerAdapter(
  options: TestRunnerAdapterOptions,
): TestRunnerAdapter {
  return {
    name: 'my-runner',

    async init(concurrencyOverride?: number) {
      // Start worker pool, set up file-swap mechanism, etc.
    },

    async runBaseline(tests: readonly string[], opts: BaselineOptions) {
      // Run all tests without mutations.
      // Return true if they pass, false otherwise.
      // If opts.collectCoverage is true, write Istanbul-format JSON.
    },

    async runMutant(mutant: MutantPayload, tests: readonly string[]) {
      // Swap in the mutated code and run the relevant tests.
      // Return { status: 'killed' | 'escaped' | 'timeout' | 'error', durationMs }
    },

    async shutdown() {
      // Tear down workers and clean up resources.
    },

    hasCoverageProvider() {
      // Return true if the runner has coverage support available.
      return false
    },

    async detectCoverageConfig() {
      // Return { perTestEnabled, coverageEnabled } from runner config.
      return { perTestEnabled: false, coverageEnabled: false }
    },
  }
}

The key requirement is the file-swap mechanism -- the adapter needs a way to intercept module resolution so the mutated source code is loaded instead of the original file on disk. See the Vitest adapter (Vite plugin + ESM loader) and Jest adapter (custom resolver) for working reference implementations in src/runner/vitest/ and src/runner/jest/.

Maintainers

License

MIT