# 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](https://github.com/debuffjp/debuff/tree/main/tooling/henka) · [Keiko's adapter](/keiko/replays-and-ci#mutate) · [Release post](/blog/introducing-henka)

## Why {#why}

Our own effects package, `@debuffjp/juice`, has 94% line coverage and nine passing tests. Henka changed one line in its particle system:

```diff
- p.x += p.vx * dt;
+ p.x -= p.vx * dt;
```

Every particle now flies the wrong way, and all nine tests still pass. Of the 266 changes Henka made to juice, 132 passed every test, and reading them one by one, 69 are real gaps in code the tests run. The numbers are in [What 94% line coverage missed](/research/what-coverage-missed).

Each miss is a line and a change that no test checks, which is exactly what the next test should cover. That makes it a good check on tests an agent wrote: give the agent `mutants.out/missed.txt` and ask for tests of the behaviour behind each line.

- No config file. Run `npx henka` in a project that tests with Vitest.
- Nothing on disk changes: the mutated code is swapped in as Vite loads the file.
- Only the test files that load the changed file run, and they stop at the first failure.
- `--since main` checks only the lines your branch changed.

## Run it {#run}

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 ` ↔ `>=`, `===` ↔ `!==`, `==` ↔ `!=` |
| Logic | `&&` ↔ `\|\|`, `!x` → `x` |
| Sign | `-x` → `x` |
| Numbers | `n` → `0`, `0` → `1` |
| Function bodies | emptied, 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 {#outcomes}

| Outcome | What it means |
| --- | --- |
| `caught` | A test failed. Something tests what this changes. |
| `missed` | Every test passed, so nothing tests what it changes. Write a test of that behaviour. |
| `held` | Every test passed under a change they should ignore. Keiko's seed mutants use this. |
| `brittle` | A test failed under a change it should ignore, such as a different random seed. |
| `unviable` | The mutant couldn't be applied. |

## What to do with a miss {#misses}

Test the behaviour the line controls, not the change Henka made. For the particle line above, a test that a particle moves by its velocity times `dt` catches both `-=` and `/`. A test that only pins the mutated value catches that one mutant and nothing else.

Some misses change nothing a test could see, such as `` | Only lines changed since a git ref. New files count in full. |
| `--jobs ` | Mutants to run at once (default: half the cores, at most 4). |
| `--timeout ` | Stop a run after this many seconds. |
| `--root ` | Where Vitest finds its config. |
| `--out ` | Where to write results (default `mutants.out`). |
| `--list` | Print the mutants without running them. |
| `--json` | Print the report as JSON. |

## Web games {#games}

`npx henka` needs Vitest. debuff games test with `debuff test`, which runs on [Keiko](/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](/keiko/replays-and-ci#mutate).

## Your own adapter {#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.