Skip to content

Henka ​

変化 (henka): change. Tests for your tests. Henka breaks your code on purpose, one small change at a time, runs your tests after each, and lists the changes no test caught. Coverage says a line ran. Henka says whether any test depends on what it does.

GitHub · Keiko's adapter

Run it ​

In a project that tests with Vitest 5 or later:

bash
npx henka                  # mutate src/
npx henka src/physics.ts   # one file or folder
npx henka --since main     # only the lines changed since main
✔ caught   src/score.ts:3:15 hits * 2 → hits / 2
✖ missed   src/score.ts:2:12 hits < 3 → hits <= 3
✖ missed   src/unused.ts:1:40 n * 2 → n / 2

missed: no test failed, so nothing tests what these change. Add a test of the behaviour they control.
  src/score.ts:2:12 hits < 3 → hits <= 3
  src/unused.ts:1:40 n * 2 → n / 2: no test loads this file

hits < 3 → hits <= 3 was missed because no test calls combo(3), the one value where the two differ. A test for it is the fix. The exit code is 1 when anything was missed, so it can gate CI, and the results are written to mutants.out/ (outcomes.json and one text file per outcome) for agents and CI to read.

What it changes ​

ChangeExample
Arithmetic+ ↔ -, * ↔ /, % → *, += ↔ -=, *= ↔ /=, ++ ↔ --
Comparison< ↔ <=, > ↔ >=, === ↔ !==, == ↔ !=
Logic&& ↔ ||, !x → x
Sign-x → x
Numbersn → 0, 0 → 1
Function bodiesemptied, or returning 0, "", true or false to match the return type

String joins, types, imports and object keys are left alone. Put // henka-skip on or above a line to skip it, or // henka-skip-file anywhere in a file.

Outcomes ​

OutcomeWhat it means
caughtA test failed. Something tests what this changes.
missedEvery test passed, so nothing tests what it changes. Write a test of that behaviour.
heldEvery test passed under a change they should ignore. Keiko's seed mutants use this.
brittleA test failed under a change it should ignore, such as a different random seed.
unviableThe mutant couldn't be applied.

How it runs ​

Henka first runs the tests related to your files with nothing changed and notes which test files load each source file. Then each job keeps a Vitest process open and runs one mutant after another. A Vite plugin swaps in the changed code as the file loads, so nothing on disk changes, and only the test files that load that file run, stopping at the first failure. A run that takes more than 5x the baseline (at least 20 s) is killed and counts as caught: the mutant hung the tests.

Option
--since <ref>Only lines changed since a git ref. New files count in full.
--jobs <n>Mutants to run at once (default: half the cores, at most 4).
--timeout <s>Stop a run after this many seconds.
--root <dir>Where Vitest finds its config.
--out <dir>Where to write results (default mutants.out).
--listPrint the mutants without running them.
--jsonPrint the report as JSON.

Web games ​

npx henka needs Vitest. debuff games test with debuff test, which runs on Keiko, so use Keiko's adapter there: npx keiko mutate changes the game rather than its code: params to each end of their range, a different random seed and a lower frame rate. See Test your tests.

Your own adapter ​

The core runs nothing itself. An adapter runs the tests and applies the mutants, so henka works with any runner:

ts
import { type Adapter, formatReport, mutate, writeOut } from "henka";

const adapter: Adapter = {
  name: "my-runner",
  baseline: () => runMyTests(),
  mutants: () => [{ id: "timeout=0", label: "request timeout 30 s → 0", expect: "noticed" }],
  run: (mutant) => runMyTests({ apply: mutant.id }),
};

const report = await mutate(adapter, { jobs: 4 });
writeOut(report, "mutants.out");
console.log(formatReport(report));

henka/source finds and applies source mutants with any ESTree parser, and henka/vite is the Vite plugin, for adapters that load code through Vite.