# Mutation testing that proves each fault landed muhar plants named faults in your source, one exact text edit each, proves on disk that every edit landed, runs your test suite, and records whether the suite went red. If the harness itself cannot be trusted — the suite was already red, or a fault it must catch went unnoticed — the whole run is discarded with exit status 2. It is never reported as a clean result. - [Install muhar](install.md) - [Run the worked example](getting-started.md) This is a real run against the small Go package in the [worked example](getting-started.md). Two guards have tests that notice when the guard is removed. One does not. ```console $ muhar -spec mutations.json Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today NOT_CAUGHT a withdrawal that lands exactly on the limit is refused Totals: 2 caught, 1 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` ## A passing test is not a test that can fail A green suite tells you the code did what the tests checked. It does not tell you whether the tests check the thing you care about. Line coverage does not either: a line can execute under a test that asserts nothing about it. Mutation testing asks the direct question. Change the code the way a bug would — flip a comparison, drop a check, return early — and run the suite. If the suite goes red, a test is guarding that behaviour. If it stays green, nothing is: the bug you just planted would ship. Tools such as [PIT](https://pitest.org/), [Stryker](https://stryker-mutator.io/), [cargo-mutants](https://mutants.rs/), [Gremlins](https://gremlins.dev/) and [mutmut](https://mutmut.readthedocs.io/) generate hundreds of such changes automatically and report a mutation score. muhar takes a narrower job and does it with stricter evidence. The [comparison](comparison.md) covers where each of them is the better choice. ## What muhar does differently ### Named guards, not a score You write each mutation yourself, as the specific guard you need proven: the daily-limit check, the authorisation check, the refusal that stops a bad merge. Each one has a name, and the report gives a verdict per name. There is no generated mutant list to triage and no percentage to argue about. The question each line answers is the one you asked: does the suite notice when this guard is gone? ### Proof that each fault landed A mutation is an exact text edit: replace this string with that one, in this file. muhar refuses an edit that cannot change anything — an empty or missing `old` string, `old` equal to `new` — writes the result, then reads the file back from disk and compares the bytes. Only a mutation proven in place is handed to the suite. One that cannot be proven is reported as `COULD_NOT_MUTATE`, its own state, and never as `NOT_CAUGHT`. ### A broken harness never reads as clean Before any verdict, muhar runs your suite on pristine source (it must pass) and then plants a positive control — a mutation the suite must catch. If either check fails, nothing after it can be believed, so muhar prints `HARNESS BROKEN — run discarded.`, gives the reason, prints no per-guard verdicts and no `Totals:` line, and exits 2. > muhar replaced an ad-hoc harness that used BSD `sed -i ''` to plant its edits. On a machine with GNU sed that invocation edits nothing, so every "mutation" ran against pristine source, every suite stayed green, and the harness reported with full confidence that no guard had a test. A false NOT CAUGHT looks exactly like a real one. muhar is built so that run cannot happen: the edit is proven on disk, and a control that is not caught discards the run. ## Three verdicts, never two | Verdict | Meaning | What to do | |---|---|---| | `CAUGHT` | The suite failed with the mutation in place. A test guards this behaviour. | Nothing. | | `NOT_CAUGHT` | The suite passed with the mutation proven in place. No test notices this guard is gone. | Write the missing test, then rerun. | | `COULD_NOT_MUTATE` | The edit could not be planted or proven, so the suite never ran against it. | Fix the spec: the `old` text drifted, or the file moved. Never read this as clean. | The [output reference](output.md) has the exact format of every line, and the [exit codes](output.md#exit-codes). ## Any file, any suite The suite is any shell command, and a mutation is a text edit, so muhar is not limited to Go source. The same spec can plant a fault in a JSON policy file, a shell script or a YAML config, and run whatever command proves it: `go test`, `make check`, `bats`. [Writing good mutants](writing-mutants.md) has examples for each. ```json { "test": "go test ./... -count=1 -timeout 60s", "control": { "name": "CONTROL: never refuse an over-limit withdrawal", "file": "limits.go", "old": "\t\treturn ErrOverLimit", "new": "\t\treturn nil" }, "mutations": [ { "name": "zero is accepted as an amount", "file": "limits.go", "old": "if amount <= 0 {", "new": "if amount < 0 {" } ] } ``` ## When muhar is the wrong tool If you want broad, automatic coverage of a codebase you do not know well, use a generator: it will find weak spots you did not think to name. muhar has no mutation operators, no score, and no coverage-guided test selection yet, and it runs your whole suite once per mutation. It earns its place on the short list of guards that must never silently lose their test, and in CI where a broken harness must fail the build rather than pass it. The [roadmap](roadmap.md) lists the cost and selection work in progress. ## Where to go next - [Install](install.md): a release binary or a build from source. - [Worked example](getting-started.md): from an empty directory to a `NOT_CAUGHT` you then fix, in five minutes. - [Spec reference](spec.md) and [CLI reference](cli.md): every field and flag. - [CI integration](ci.md): sharded legs and the `Totals:` line as a gate. - muhar ships with [Assay](https://assay.guide/), a delivery workflow for coding agents, whose release pipeline uses it to prove that the desk tools' refusals are tested. --- Source of this page: https://muhar.assay.guide/ # Install muhar is a single static binary. There are three ways to get it: take it from a release bundle, build it from source, or run it from a clone without installing anything. All three produce the same program. ## Requirements | Requirement | Why | |---|---| | `sh` on `PATH` | muhar runs the spec's `test` command with `sh -c`, so pipes and `&&` work. On Windows this means a POSIX shell such as the one Git for Windows installs. | | Go 1.25 or later, only to build from source | The module muhar lives in declares `go 1.25.0`. A release binary needs no Go toolchain. | | `go` on `PATH`, only for some parallel runs | With `-j` above 1, muhar rewrites relative `replace` directives in copied `go.mod` files using `go mod edit`. Only a tree whose `go.mod` contains a `replace` needs it. See [How a run works](internals.md#module-replace). | | Free space in the temp directory | Each parallel worker gets a full copy of the spec's root; every run gets a throwaway Go build cache. | ## From a release bundle muhar is built and published with every release of [medici-finance/assay](https://github.com/medici-finance/assay/releases), inside the `desk-tools-.tar.gz` bundle together with the other desk tools. Bundles exist for `linux-amd64`, `darwin-arm64`, `darwin-amd64`, `windows-amd64` and `windows-arm64`. There is no `linux-arm64` bundle yet; build from source there. On Linux or macOS, download the bundle and the release's checksum list, verify, and extract only `muhar`. Set `PLATFORM` to match your machine: ```sh PLATFORM=linux-amd64 BASE=https://github.com/medici-finance/assay/releases/latest/download curl -fsSLO "$BASE/desk-tools-$PLATFORM.tar.gz" curl -fsSLO "$BASE/checksums.txt" grep " desk-tools-$PLATFORM.tar.gz\$" checksums.txt | sha256sum -c - tar -xzf "desk-tools-$PLATFORM.tar.gz" ./muhar mkdir -p "$HOME/.local/bin" install -m 0755 muhar "$HOME/.local/bin/muhar" export PATH="$HOME/.local/bin:$PATH" ``` On macOS use `shasum -a 256 -c -` in place of `sha256sum -c -`. The check prints `desk-tools-.tar.gz: OK`; anything else means the download is not the published file, so stop. The `PATH` change above applies to this shell. Add the same export to your shell's startup file to keep it in future sessions. On Windows, choose `windows-amd64` or `windows-arm64`. The archive member is `./muhar.exe`; extract that binary, put it in a directory on `PATH`, and keep a POSIX `sh` on `PATH` as described in [Requirements](#requirements). The Unix `install` command above is not a PowerShell recipe. For CI, pin a release instead of `latest`, so a new release cannot change your gate without a commit: ```sh BASE=https://github.com/medici-finance/assay/releases/download/v1.0.39 ``` The bundle's version is the Assay release tag; muhar has no version number of its own today. ## From source muhar lives in the `tools/desk` Go module of the Assay repository. Build it from a clone: ```sh git clone https://github.com/medici-finance/assay.git cd assay/tools/desk go install ./cmd/muhar ``` The binary lands in `$(go env GOBIN)`, or `$(go env GOPATH)/bin` when `GOBIN` is unset. Add that directory to `PATH`. > `go install github.com/medici-finance/assay/tools/desk/cmd/muhar@latest` does not work. The `tools/desk` module uses a `replace` directive that points at another directory of the repository, and the go command refuses `go install module@version` for a module with `replace` directives. Build from a clone as above. To build a specific release, check out its tag before `go install`: ```sh git -C assay checkout v1.0.39 ``` ## Without installing In a clone of the Assay repository, `go run` compiles and runs muhar in one step. This is how Assay's own CI calls it: ```sh cd assay/tools/desk go run ./cmd/muhar -spec /path/to/mutations.json ``` `go run` is a wrapper: it returns 0 when the program succeeds, but a muhar exit of 1 or 2 is reported as shell status **1**. For example, an unknown flag makes the binary return 2; `go run` instead prints `exit status 2` on stderr and returns 1. A broken harness's 2 is also collapsed to 1. See [Go's run documentation](https://pkg.go.dev/cmd/go#hdr-Compile_and_run_Go_program). Use the released or locally built binary when your script needs the exact [exit codes](output.md#exit-codes). To build without installing into `PATH`: ```sh cd assay/tools/desk go build -o ./muhar ./cmd/muhar ./muhar -spec /path/to/mutations.json ``` Invoke the binary directly to preserve muhar's numeric exit status. Compilation errors from `go run` also return nonzero before muhar runs, so read stderr to distinguish them from a muhar failure. ## Check the install `muhar -h` prints the three flags and exits 0. Running it with no spec is a usage error, exit 1: ```console $ muhar muhar: -spec is required $ echo $? 1 ``` Continue with the [worked example](getting-started.md). ## Coming These are planned and not available yet. They are tracked in the open and listed on the [roadmap](roadmap.md). - A standalone muhar README and a one-line install that fetches only muhar, not the whole desk-tools bundle ([assay#2638](https://github.com/medici-finance/assay/issues/2638)). - A reusable CI action that installs muhar, runs a sharded sweep and gates on the result ([assay#2642](https://github.com/medici-finance/assay/issues/2642)). ## Licence muhar is part of the Assay repository and is licensed under the Apache License, Version 2.0. Source: [tools/desk/cmd/muhar](https://github.com/medici-finance/assay/tree/main/tools/desk/cmd/muhar). --- Source of this page: https://muhar.assay.guide/install # Worked example This page builds a small Go package with three guards, writes a spec that removes each guard in turn, finds the one with no test, fixes it, and then breaks the harness on purpose to show what muhar does instead of reporting a clean result. Every output below was captured from a real run. You need muhar on `PATH` ([Install](install.md)) and a Go toolchain. ## The code under test A withdrawal check with three rules: the amount must be positive, and the day's total must not exceed the limit — where landing exactly on the limit is allowed. ```sh mkdir limits && cd limits go mod init example.com/limits ``` `limits.go`: ```go package limits import "errors" var ( ErrNonPositive = errors.New("amount must be positive") ErrOverLimit = errors.New("amount exceeds the daily limit") ) // Check reports whether amount may be withdrawn, given what has // already been withdrawn today and the daily limit. func Check(amount, spentToday, limit int64) error { if amount <= 0 { return ErrNonPositive } if spentToday+amount > limit { return ErrOverLimit } return nil } ``` `limits_test.go`: ```go package limits import ( "errors" "testing" ) func TestRejectsNonPositive(t *testing.T) { for _, amount := range []int64{0, -5} { if err := Check(amount, 0, 100); !errors.Is(err, ErrNonPositive) { t.Errorf("Check(%d, 0, 100) = %v, want ErrNonPositive", amount, err) } } } func TestRejectsOverLimit(t *testing.T) { if err := Check(20, 90, 100); !errors.Is(err, ErrOverLimit) { t.Errorf("Check(20, 90, 100) = %v, want ErrOverLimit", err) } } func TestAllowsWithinLimit(t *testing.T) { if err := Check(10, 50, 100); err != nil { t.Errorf("Check(10, 50, 100) = %v, want nil", err) } } ``` `go test -cover ./...` passes and reports `coverage: 100.0% of statements`. Neither fact says whether every rule has a test. ## The spec `mutations.json` names each guard and the edit that removes it. The control is a fault the suite must catch: if the over-limit refusal is replaced by `return nil`, `TestRejectsOverLimit` has to fail. ```json { "test": "go test ./... -count=1 -timeout 60s", "control": { "name": "CONTROL: never refuse an over-limit withdrawal", "file": "limits.go", "old": "\t\treturn ErrOverLimit", "new": "\t\treturn nil" }, "mutations": [ { "name": "zero is accepted as an amount", "file": "limits.go", "old": "if amount <= 0 {", "new": "if amount < 0 {" }, { "name": "the limit ignores what was already spent today", "file": "limits.go", "old": "if spentToday+amount > limit {", "new": "if amount > limit {" }, { "name": "a withdrawal that lands exactly on the limit is refused", "file": "limits.go", "old": "spentToday+amount > limit", "new": "spentToday+amount >= limit" } ] } ``` Three details worth noticing. `-count=1` stops `go test` from serving a cached pass. The control's `old` starts with two tab characters, written `\t\t` in JSON, because the edit is matched byte for byte. The `file` paths are relative and there is no `root`, so they resolve against the directory you run muhar from. The [spec reference](spec.md) covers each rule. ## Run it ```console $ muhar -spec mutations.json Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today NOT_CAUGHT a withdrawal that lands exactly on the limit is refused Totals: 2 caught, 1 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` The report goes to stdout. The suite's own output goes to stderr, one block per suite run, in order: the baseline, the control, then each mutation. ```text ok example.com/limits 0.074s --- FAIL: TestRejectsOverLimit (0.00s) limits_test.go:18: Check(20, 90, 100) = , want ErrOverLimit FAIL FAIL example.com/limits 0.061s FAIL --- FAIL: TestRejectsNonPositive (0.00s) limits_test.go:11: Check(0, 0, 100) = , want ErrNonPositive FAIL FAIL example.com/limits 0.068s FAIL --- FAIL: TestRejectsOverLimit (0.00s) limits_test.go:18: Check(20, 90, 100) = , want ErrOverLimit FAIL FAIL example.com/limits 0.067s FAIL ok example.com/limits 0.046s ``` The last suite run passed with `>` turned into `>=`: no test checks that a withdrawal landing exactly on the limit is allowed. Exit status 0 means the harness was healthy, not that every guard was caught. A gate on the result reads the `Totals:` line; see [CI integration](ci.md). ## Fix the gap Add the missing boundary test to `limits_test.go`: ```go func TestAllowsExactlyTheLimit(t *testing.T) { if err := Check(10, 90, 100); err != nil { t.Errorf("Check(10, 90, 100) = %v, want nil", err) } } ``` ```console $ muhar -spec mutations.json Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today CAUGHT a withdrawal that lands exactly on the limit is refused Totals: 3 caught, 0 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` That `Totals:` line, with `0 NOT CAUGHT, 0 could-not-mutate`, is the clean result. ## A mutation that does not land Specs drift. Add two broken entries to the end of `mutations` — one whose `old` text is not in the file, one whose file does not exist: ```json { "name": "stale anchor", "file": "limits.go", "old": "if amount <= 0 {\n\t\treturn ErrNegative", "new": "x" }, { "name": "missing file", "file": "limit.go", "old": "a", "new": "b" } ``` Run against the original test file (without the boundary test from step 4): ```console $ muhar -spec mutations.json Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today NOT_CAUGHT a withdrawal that lands exactly on the limit is refused COULD_NOT_MUTATE stale anchor (mutation "stale anchor": Old text not found in limits.go — edit did not land) COULD_NOT_MUTATE missing file (mutation "missing file": read limit.go: open limit.go: no such file or directory) Totals: 2 caught, 1 NOT CAUGHT, 2 could-not-mutate. NOTE: could-not-mutate is NOT a clean gate — those guards were never actually tested. $ echo $? 0 ``` Neither broken entry ran the suite, so neither can be mistaken for a guard with no test, and the run still exits 0: the harness itself is fine. The `NOTE:` line and the non-zero `could-not-mutate` count are what a gate must refuse. ## A harness muhar will not trust Three ways to break the harness, and the run each one produces. Each is exit status 2, with no per-guard verdicts and no `Totals:` line. The control's `old` text is wrong, so the control cannot be planted: ```console $ muhar -spec mutations.json HARNESS BROKEN — run discarded. positive control could not be planted (mutation "CONTROL: never refuse an over-limit withdrawal": Old text not found in limits.go — edit did not land) — the harness is not mutating; discard the run $ echo $? 2 ``` The control is an edit no test can see — renaming the text of an error that the tests compare by identity with `errors.Is`: ```json "control": { "name": "CONTROL: rename an error message", "file": "limits.go", "old": "amount must be positive", "new": "amount must be > 0" } ``` ```console $ muhar -spec mutations.json HARNESS BROKEN — run discarded. positive control was NOT CAUGHT — a mutation the suite must catch stayed green, so the harness is broken (not mutating, or the suite cannot see the source); discard the run $ echo $? 2 ``` The suite fails before anything is mutated — here a `test` command that ends in `&& false`: ```console $ muhar -spec mutations.json HARNESS BROKEN — run discarded. baseline suite is RED on pristine source — cannot distinguish a caught mutation from a pre-existing failure; discard the run $ echo $? 2 ``` ## Faster runs `-j 2` runs two mutations at a time, each in its own copy of the tree. The report is identical, line for line; muhar names the copied root on stderr. ```console $ muhar -j 2 -spec mutations.json muhar: 2 mutations in flight, one isolated copy of /home/dev/limits per worker Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today NOT_CAUGHT a withdrawal that lands exactly on the limit is refused Totals: 2 caught, 1 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` `-shard i/n` splits the mutations across separate invocations, for CI legs on separate machines. Each shard reruns the baseline and the control, and reports only its own mutations: ```console $ muhar -shard 0/2 -spec mutations.json muhar: shard 0/2 — running 2 of 3 mutations (indices ≡ 0 mod 2); the other shards own the rest Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount NOT_CAUGHT a withdrawal that lands exactly on the limit is refused Totals: 1 caught, 1 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` ```console $ muhar -shard 1/2 -spec mutations.json muhar: shard 1/2 — running 1 of 3 mutations (indices ≡ 1 mod 2); the other shards own the rest Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT the limit ignores what was already spent today Totals: 1 caught, 0 NOT CAUGHT, 0 could-not-mutate. $ echo $? 0 ``` Next: the [concepts](concepts.md) behind each line, or straight to [CI integration](ci.md). --- Source of this page: https://muhar.assay.guide/getting-started # Concepts muhar's vocabulary is small. Each term below is used with exactly this meaning everywhere on this site and in muhar's output. ## Guard A guard is a piece of behaviour that must hold: a check that refuses a bad input, a limit, an authorisation test, an invariant in a config file. Guards are what you care about; mutations are how muhar asks whether a test protects them. ## Mutation A mutation is one exact text edit that removes or weakens a guard: in file `file`, replace the first occurrence of `old` with `new`. Each mutation has a `name`, which is how it appears in the report — so name it after the bug it simulates ("zero is accepted as an amount"), not after the edit. muhar does not generate mutations. You write them. That is the trade: fewer mutations than a generator would produce, each one chosen, named and meaningful, with no equivalent-mutant noise to triage. See [Writing good mutants](writing-mutants.md). ## Suite The suite is the spec's `test` command, run with `sh -c` in the spec's `root` (or the current directory). A non-zero exit status means the suite failed, which, with a mutation in place, means the mutation was caught. muhar does not parse test output; the exit status is the whole signal. ## Baseline Before anything is mutated, muhar runs the suite once on pristine source. It must pass. If it fails, any later "caught" could be the pre-existing failure rather than the mutation, so the run is discarded. ## Positive control The control is a mutation the suite must catch: a fault so central that a working harness and a working suite cannot miss it. muhar plants it after the baseline. If it cannot be planted, or the suite stays green with it in place, then either muhar is not mutating what the suite builds or the suite cannot see the source. Either way, no verdict from the run can be trusted, and the run is discarded. A harness with no failing control is a green light with no bulb: it looks the same whether it works or not. The control is the bulb test, run on every invocation. ## The proven-landed guarantee A mutation counts as applied only when muhar has proven it on disk. Before writing, muhar refuses an edit that cannot change the file: `old` is empty, `old` equals `new`, or `old` is absent. After writing, it reads the file back and requires the bytes on disk to equal the intended mutated content and to differ from the original. Only then does it run the suite. Every failure along the way becomes `COULD_NOT_MUTATE`, with the reason, and the suite is never run against that mutation. The point is that `NOT_CAUGHT` always means "the fault was in place and the suite still passed". It never means "the edit silently did nothing". ## Verdicts Each mutation gets exactly one of three verdicts. The third is a separate state on purpose: a fault that was never planted has not been tested, and folding it into either of the other two would report something that did not happen. | Verdict | Condition | Called in the source | |---|---|---| | `CAUGHT` | Mutation proven in place; suite exited non-zero. | checked-failed | | `NOT_CAUGHT` | Mutation proven in place; suite exited zero. A real finding. | checked-clean | | `COULD_NOT_MUTATE` | Mutation could not be planted or proven; the suite did not run. | could-not-check | A mutation that does not compile makes `go test` fail, so it is reported `CAUGHT` today. That is a known limitation, and the [roadmap](roadmap.md) has the fix. ## Harness healthy, harness broken A run is healthy when the baseline passed and the control was caught. Only a healthy run prints per-guard verdicts and a `Totals:` line, and exits 0. A broken run prints `HARNESS BROKEN — run discarded.` and one reason, and exits 2. A healthy run is not a clean result. Exit 0 says the instrument worked; the verdicts say what it found. A gate needs both: exit status 0, and a `Totals:` line with `0 NOT CAUGHT, 0 could-not-mutate`. See [Output and exit codes](output.md). ## Why exit 2 is never clean Exit 2 means muhar could not establish that it was measuring anything. Reading it as a pass — "no guards were reported missing" — repeats the failure muhar was built to prevent: a harness that silently tests nothing and reports confidently. A CI step must fail on exit 2. Because Go's flag parser also exits 2 on an unknown flag or a malformed flag value, the safe rule is to fail on any non-zero exit. ## Run order Figure: The order of one run. A failure in the first two steps discards the run with exit 2; a mutation that cannot be planted is recorded and the run continues. 1. Baseline: run the suite on pristine source. It must pass, or the run is discarded (exit 2). 2. Positive control: plant, prove on disk, run the suite, restore. It must be CAUGHT, or the run is discarded (exit 2). 3. Each mutation, in spec order: plant, prove on disk, run the suite, restore. A mutation that cannot be planted or proven is COULD_NOT_MUTATE and the suite is not run. 4. Report: per-mutation verdicts in spec order, then the Totals line. Exit 0. The details of each step, including what happens with `-j` above 1, are in [How a run works](internals.md). --- Source of this page: https://muhar.assay.guide/concepts # Spec reference A spec is one JSON file, passed with `-spec`. It names the suite command, the positive control, and the mutations to try. A [JSON Schema](schema/spec.v1.json) for editors and linters is published at `https://muhar.assay.guide/schema/spec.v1.json`. ```json { "$schema": "https://muhar.assay.guide/schema/spec.v1.json", "root": "/home/dev/limits", "test": "go test ./... -count=1 -timeout 60s", "control": { "name": "CONTROL: never refuse an over-limit withdrawal", "file": "limits.go", "old": "\t\treturn ErrOverLimit", "new": "\t\treturn nil" }, "mutations": [ { "name": "zero is accepted as an amount", "file": "limits.go", "old": "if amount <= 0 {", "new": "if amount < 0 {" } ] } ``` ## Top-level fields | Field | Type | Required | Meaning | |---|---|---|---| | `root` | string | no | Directory the suite runs in and relative `file` paths resolve against. Empty or absent: the current working directory. A relative `root` is itself relative to the current directory. | | `test` | string | yes | Suite command, run as `sh -c ''` in `root`. Exit status non-zero means the suite failed. Must be non-empty. | | `control` | mutation | yes | The [positive control](concepts.md#control): a mutation the suite must catch. Its `name` and `old` must be non-empty. | | `mutations` | array of mutation | no | The guards under test, run and reported in this order. Empty or absent: only the baseline and control run, and the report says `Totals: 0 caught, 0 NOT CAUGHT, 0 could-not-mutate.` | ## Mutation fields `control` and every entry of `mutations` have the same shape. | Field | Type | Meaning | |---|---|---| | `name` | string | The label in the report. Name the bug the edit simulates. Not required to be unique, but a duplicate makes the report ambiguous. | | `file` | string | File to edit. Absolute: used as is. Relative: joined to `root`, or to the current directory when `root` is empty. | | `old` | string | Exact text to replace. Must be non-empty and present in the file. | | `new` | string | Replacement text. Must differ from `old`. May be empty, to delete `old`. | ## Exact match, first occurrence `old` is matched byte for byte: no regular expressions, no whitespace folding, no trimming. muhar replaces the first occurrence only. If the same text appears more than once and you mean a later one, widen `old` with surrounding context until it is unique, or until its first occurrence is the one you mean. The worked example relies on this. The mutation "a withdrawal that lands exactly on the limit is refused" uses `old` `spentToday+amount > limit`, which also occurs inside the previous mutation's `old`. Each mutation is planted on pristine source, so they do not interfere. ## Whitespace and line endings Write tabs and newlines as JSON escapes: `\t`, `\n`. A file with Windows line endings needs `\r\n` in any `old` that spans a line break; a `\n`-only `old` will not match it, and the mutation is reported `COULD_NOT_MUTATE` with `Old text not found`. Non-ASCII text is matched as UTF-8 bytes. ## What happens to the file For each mutation muhar reads the file, computes the edit, writes the result with the file's existing permission bits, reads it back to prove the bytes landed, runs the suite, and then writes the original bytes back. Restoration is unconditional once the write has happened, whatever the verdict. A symbolic link is followed: the edit lands in the link's target. One exception: an interrupt (`SIGINT`, `SIGTERM`) during a sequential run ends the process before the restore, so the file under test is left mutated. See [How a run works](internals.md#signals). ## Absolute paths and parallel runs With `-j` above 1, each worker runs the suite in its own copy of `root`. A relative `file` resolves inside that copy. An absolute `file` does not: every worker edits the same original file while running the suite in its copy, which produces wrong verdicts. Use relative paths in any spec you run with `-j` above 1 or `-j 0`. This is tracked as [assay#2643](https://github.com/medici-finance/assay/issues/2643). ## Decoding rules muhar decodes the spec with Go's `encoding/json`. Three consequences: - Unknown keys are ignored. That is what makes `$schema` safe to include, and also means a misspelt key such as `"mutation"` is silently dropped. The published schema is stricter and rejects unknown keys, so validate against it to catch typos. - Key matching is case-insensitive: `"Test"` and `"TEST"` both set `test`. - A wrong type is an error: `{"test": 1}` fails with `json: cannot unmarshal number into Go struct field spec.test of type string`. ## Spec errors These stop muhar before any suite runs. Each is printed to stderr and exits 1. | Message | Cause | |---|---| | `muhar: read spec: ` | The `-spec` file cannot be read. | | `muhar: parse spec: ` | The file is not valid JSON, or a field has the wrong type. | | `muhar: spec.test (the suite command) is required` | `test` is empty or absent. | | `muhar: spec.control is required (a mutation the suite MUST catch) — a harness with no failing control is a green light with no bulb (#34)` | `control.name` or `control.old` is empty or absent. | ## COULD_NOT_MUTATE reasons When a mutation cannot be planted or proven, the report line carries the reason in parentheses. `` is the mutation's name, `` is `file` as written in the spec, and `` is the resolved path. | Reason | Cause | |---|---| | `mutation "": Old is empty — nothing to match, edit cannot land` | `old` is empty. | | `mutation "": Old == New — edit is a no-op` | `old` and `new` are identical. | | `mutation "": Old text not found in — edit did not land` | The file does not contain `old`. The usual cause is drift: the code changed and the spec did not. | | `mutation "": content unchanged after replace — silent no-op` | A final check that the edited content differs from the original. | | `mutation "": read : ` | The file cannot be read; also `stat`, `write` and `re-read` variants. | | `mutation "": post-write verify failed — on-disk content does not match the intended mutation` | The bytes read back differ from the bytes written. | | `mutation "": post-write verify failed — file is byte-identical to the original` | The bytes read back equal the original. | The same reasons apply to the control, but there they discard the run: `positive control could not be planted () — the harness is not mutating; discard the run`, exit 2. ## JSON Schema [`/schema/spec.v1.json`](schema/spec.v1.json) is a JSON Schema (draft 2020-12) for this format. It requires `test` and `control`, requires a non-empty `name`, `file` and `old` on the control and on every mutation, and rejects unknown keys. It is deliberately stricter than muhar's decoder; a spec that passes it is always accepted by muhar. To validate in an editor, add `"$schema": "https://muhar.assay.guide/schema/spec.v1.json"` as the first key. With a command-line validator: ```sh check-jsonschema --schemafile https://muhar.assay.guide/schema/spec.v1.json mutations.json ``` --- Source of this page: https://muhar.assay.guide/spec # CLI reference ```text muhar -spec [-j ] [-shard /] ``` muhar uses Go's standard flag syntax: `-j 2`, `-j=2` and `--j 2` are equivalent. Flags must come before any positional argument; muhar takes none. ## Flags | Flag | Default | Meaning | |---|---|---| | `-spec ` | none, required | Path to the [spec](spec.md) JSON, relative to the current directory. Missing: `muhar: -spec is required`, exit 1. | | `-j ` | `1` | Mutations in flight at once. `1` runs sequentially, in place. With at least two effective workers, each gets its own copy of the tree. `0` sizes to the machine, capped at 2. Negative: `muhar: -j must be >= 0`, exit 1. | | `-shard /` | none: run every mutation | Run only this invocation's share: the mutations at spec index ≡ i (mod n), 0-based. Baseline and control still run in full. | | `-h`, `-help` | | Print usage to stderr and exit 0. | ## -j: parallel workers A sweep is one suite run per mutation, so the suite is almost the whole cost. With `-j` above 1 and at least two mutations, the mutations are dealt to a pool of workers. Each worker owns an isolated copy of the tree, so no two workers ever edit the same file; sharing one would make every verdict depend on timing. | Value | Workers | |---|---| | `1` (default) | One, in place, in `root`. No copies. | | `n` above 1 | At most `n`, capped at the number of mutations. Two or more get their own copies of `root` in the system temp directory; one runs in place. | | `0` | One per CPU, capped at 2. On a one-CPU machine, one. | When the spec (or shard) has fewer mutations than `-j`, muhar uses that many workers; with one, it runs in place like `-j 1`. This also means an interrupted run may leave the original file mutated ([signals and cleanup](internals.md#signals)), even when the requested `-j` is above 1. Nothing about the verdicts changes with `-j`. The baseline and control still run first, in the first worker's copy when at least two workers are used, or in place otherwise, so the copy itself is proven sound before any verdict rests on it. Results are reported in spec order, whichever worker finished first. The report on stdout is byte-identical to a sequential run's. With `-j` above 1 muhar prints one line to stderr naming the tree it copies: ```text muhar: 2 mutations in flight, one isolated copy of /home/dev/limits per worker ``` The number is the requested pool size; with fewer mutations than that, fewer workers start. The notice is still printed when the effective count is one and execution falls back to running in place. ### Why -j 0 is capped at 2 Every worker holds a full copy of the tree and runs its own suite process, so peak memory grows with the number of mutations in flight, not with the size of the spec. Sizing the pool to the CPU count on a many-core CI runner put enough copies and builds in memory at once to exceed the runner's memory limit and kill the sweep. Capping `-j 0` at 2 bounds peak memory by construction. An explicit `-j ` is used as given: a caller who asks for 8 is making their own memory trade. ### Copies and Go modules A copied `go.mod` with a relative `replace` directive pointing outside the tree would resolve to nothing in the temp directory, and every suite run in the copy would fail. muhar rewrites those directives to absolute paths in each copy and re-reads the file to prove the rewrite landed. This needs `go` on `PATH`. Details: [How a run works](internals.md#module-replace). Use relative `file` paths with `-j` above 1. An absolute `file` points at the original tree, not the worker's copy ([spec reference](spec.md#absolute-paths)). ## -shard: splitting a spec across machines `-shard i/n` runs the mutations whose 0-based index in the spec satisfies `index mod n = i`. For a fixed `n`, the `n` shards are pairwise disjoint and their union is the whole spec: every mutation runs exactly once across the set. Round-robin by index, rather than contiguous blocks, spreads neighbouring mutations — which tend to hit the same file and cost about the same — evenly across shards. muhar's test `TestShardSelectPartitions` holds the partition property. Figure: Nine mutations split with -shard i/3. Each shard runs the baseline and the control in full, then only its own mutations. Shard 0/3 runs mutation indices 0, 3, 6. Shard 1/3 runs mutation indices 1, 4, 7. Shard 2/3 runs mutation indices 2, 5, 8. Every shard also runs the baseline and the positive control. Each shard is independently trustworthy: it proves the baseline and the control before reporting anything, so a broken harness fails every shard, not one of them. Its `Totals:` line covers its own mutations only. A shard's report is a partial sweep by design, and muhar says so on stderr: ```text muhar: shard 0/2 — running 2 of 3 mutations (indices ≡ 0 mod 2); the other shards own the rest ``` Running all `n` shards is the caller's job. [CI integration](ci.md#sharded) shows a matrix that does it. `-shard` and `-j` combine: the shard is selected first, then its mutations are dealt to `-j` workers. ### Shard errors All exit 1. A malformed shard is refused; it never degrades to running everything or nothing. | Input | Message | |---|---| | `-shard 1` | `muhar: -shard must be "i/n" (0-based shard i of n), got "1"` | | `-shard x/3` | `muhar: -shard "x/3": shard index "x" is not an integer` | | `-shard 0/x` | `muhar: -shard "0/x": shard count "x" is not an integer` | | `-shard 0/0` | `muhar: -shard "0/0": shard count must be >= 1` | | `-shard 2/2` | `muhar: -shard "2/2": shard index must satisfy 0 <= i < n` | ## Streams | Stream | Carries | |---|---| | stdout | The report only: the health line, one line per mutation, `Totals:`, and the `NOTE:` line when any mutation could not be planted. Or, for a broken run, `HARNESS BROKEN — run discarded.` and the reason. | | stderr | muhar's own notices (`-j` and `-shard` lines, errors) and everything the suite prints, on both its stdout and stderr, in run order. | Capture stdout to gate on; send stderr to the log. `report=$(muhar -spec mutations.json)` does exactly that in a shell. ## Environment The suite inherits muhar's environment with one change: `GOCACHE` is set to a fresh directory created for this run and removed when it ends. Any `GOCACHE` you set is overridden for the suite. `GOMODCACHE` is not touched, so module downloads are shared and not repeated. The temp directory for worker copies and the build cache is the system default, `TMPDIR` on Unix. Point `TMPDIR` at a disk with room for `-j` copies of the tree. Why the per-run cache exists is explained in [How a run works](internals.md#gocache). ## Exit status | Status | Meaning | |---|---| | `0` | Harness healthy. Read the report for verdicts. | | `1` | Usage, spec or I/O error. No suite ran, or setup failed. | | `2` | `HARNESS BROKEN`: baseline red, control not caught, or a worker copy could not be made. Also: an unknown flag or malformed flag value, from Go's flag parser. | | `130` | Interrupted by `SIGINT` or `SIGTERM`. | The full table, and how to gate on it, is in [Output and exit codes](output.md#exit-codes). --- Source of this page: https://muhar.assay.guide/cli # Output and exit codes muhar's stdout is a report meant to be read by people and parsed by scripts. Its format is fixed: everything on this page is taken from the code that prints it. The suite's own output goes to stderr ([Streams](cli.md#streams)). ## A healthy run ```console $ muhar -spec mutations.json Harness healthy: baseline GREEN, positive control CAUGHT. CAUGHT zero is accepted as an amount CAUGHT the limit ignores what was already spent today NOT_CAUGHT a withdrawal that lands exactly on the limit is refused COULD_NOT_MUTATE stale anchor (mutation "stale anchor": Old text not found in limits.go — edit did not land) Totals: 2 caught, 1 NOT CAUGHT, 1 could-not-mutate. NOTE: could-not-mutate is NOT a clean gate — those guards were never actually tested. $ echo $? 0 ``` | Line | Format | Present | |---|---|---| | Health | `Harness healthy: baseline GREEN, positive control CAUGHT.` | Always, first. | | Verdict | Two spaces, the verdict left-aligned in a 16-character field, one space, the mutation's `name`. A `COULD_NOT_MUTATE` line adds two spaces and the reason in parentheses. | One per mutation, in spec order. | | Totals | `Totals: caught, NOT CAUGHT, could-not-mutate.` | Always, after the verdicts. | | Note | `NOTE: could-not-mutate is NOT a clean gate — those guards were never actually tested.` | Only when `` is above 0. | The verdict words are `CAUGHT`, `NOT_CAUGHT` and `COULD_NOT_MUTATE`, with underscores. The `Totals:` line spells the middle count `NOT CAUGHT`, with a space; grep for exactly what is printed. The reasons are listed in the [spec reference](spec.md#cnm-reasons). With `-shard`, the counts cover the shard's own mutations only. ## A broken run ```console $ muhar -spec mutations.json HARNESS BROKEN — run discarded. baseline suite is RED on pristine source — cannot distinguish a caught mutation from a pre-existing failure; discard the run $ echo $? 2 ``` A broken run prints two lines: `HARNESS BROKEN — run discarded.` and the reason, indented two spaces. It prints no verdicts and no `Totals:` line, because those verdicts are exactly what cannot be trusted. A gate that looks for a `Totals:` line will therefore never pass a broken run, even if it ignores the exit status. | Reason | Cause | |---|---| | `baseline suite is RED on pristine source — cannot distinguish a caught mutation from a pre-existing failure; discard the run` | The suite failed before anything was mutated. Fix the suite or the `test` command first. | | `positive control could not be planted () — the harness is not mutating; discard the run` | The control's edit could not be made or proven. Usually its `old` text has drifted. | | `positive control was NOT CAUGHT — a mutation the suite must catch stayed green, so the harness is broken (not mutating, or the suite cannot see the source); discard the run` | The suite passed with the control in place. The control is too weak, the suite does not build the mutated file, or `root` points at a different tree. | | `could not provision isolated workspace () — a parallel sweep cannot share a tree; discard the run` | With `-j` above 1, a worker's copy of the tree could not be made: disk full, unreadable file, or a `go.mod` rewrite that failed. | ## Exit codes | Status | Meaning | Report on stdout | |---|---|---| | `0` | Harness healthy. Verdicts are trustworthy. This includes runs with `NOT_CAUGHT` or `COULD_NOT_MUTATE` lines. | Full report with `Totals:`. | | `1` | Usage, spec or setup error: missing `-spec`, unreadable or invalid spec, missing `test` or `control`, bad `-shard`, negative `-j`, temp directory not creatable. | None. Message on stderr. | | `2` | `HARNESS BROKEN`: no verdict from this run may be used. Also returned by Go's flag parser for an unknown flag or a malformed flag value such as `-j two`. | `HARNESS BROKEN` and reason, or nothing for a flag error. | | `130` | Interrupted by `SIGINT` or `SIGTERM`. | None. | Exit 0 is not a pass. It says the instrument worked. Whether the guards held is in the `Totals:` line. Exit 2 is never a pass. A broken harness reports nothing about your tests, and reading its silence as "no missing tests" is the failure muhar exists to prevent. The flag parser sharing status 2 is a known collision, tracked as [assay#2644](https://github.com/medici-finance/assay/issues/2644); it does not change the rule below. ## Gating on the result The rule: fail on any non-zero exit status, then require a `Totals:` line with zero `NOT CAUGHT` and zero `could-not-mutate`. In bash: ```bash status=0 report=$(muhar -spec mutations.json) || status=$? printf '%s\n' "$report" if [ "$status" -ne 0 ]; then echo "muhar exited $status: harness broken or usage error, no verdict" >&2 exit 1 fi if ! grep -qE '^Totals: [0-9]+ caught, 0 NOT CAUGHT, 0 could-not-mutate\.$' <<<"$report"; then echo "a guard has no test, or a mutation did not land" >&2 exit 1 fi ``` Against the runs in the [worked example](getting-started.md), this script passes only the run after the boundary test is added. It fails the `NOT_CAUGHT` run, the `COULD_NOT_MUTATE` run and all three broken runs. ## Parsing the report For scripts and agents, every verdict line matches: ```text ^ (CAUGHT|NOT_CAUGHT|COULD_NOT_MUTATE) +(.+)$ ``` The name runs to the end of the line, except on a `COULD_NOT_MUTATE` line, where the reason follows two spaces and an opening parenthesis: ` (mutation "": …)`. The name is free text, so keep names free of the sequence two-spaces-parenthesis to keep the split unambiguous. The report has no machine-readable format other than this text today. --- Source of this page: https://muhar.assay.guide/output # How a run works This page follows one invocation from start to exit. It is written against the source in [tools/desk/cmd/muhar](https://github.com/medici-finance/assay/tree/main/tools/desk/cmd/muhar): `main.go` (flags, spec, environment, exit), `harness.go` (the run, the verdicts, the report) and `workspace.go` (worker copies). ## Start-up 1. Parse flags. A flag error exits 2 from Go's flag parser. 2. Read and decode the spec. Require a non-empty `test` and a control with a `name` and `old`. Any failure exits 1. 3. With `-shard i/n`: keep only the mutations at index ≡ i (mod n) and print the selection to stderr. 4. Resolve `-j`: `0` becomes the CPU count capped at 2; negative exits 1. 5. Create the per-run Go build cache, a fresh temp directory named `assay-mutant-gocache-*`. 6. Install a handler for `SIGINT` and `SIGTERM` ([Signals](#signals)). 7. With `-j` above 1: name the tree to copy (`root`, or the current directory) and print the `-j` line to stderr. ## Baseline and control The suite runs once on pristine source. A non-zero exit discards the run. Then the control is planted exactly like a mutation (below) and the suite runs again. Anything other than `CAUGHT` discards the run. A discarded run prints its reason, removes the build cache and exits 2 without touching any other mutation. With at least two effective workers, both steps run inside the first worker's copy, not in the original tree. This proves the copy builds and tests the same as the original before any verdict depends on it. The worker count is capped at the number of mutations in the spec (or shard); with one, execution falls back to running in place like `-j 1`, including the baseline and control. ## Planting one mutation For the control and for each mutation, in a given tree: 1. Resolve `file`: absolute as is, relative joined to the tree's root. 2. Read the file. Failure: `COULD_NOT_MUTATE`. 3. Compute the edit in memory. Refuse an empty `old`, `old` equal to `new`, `old` not found, or a result identical to the input: `COULD_NOT_MUTATE`. 4. Write the edited content with the file's existing permission bits. 5. Read the file back. If the bytes differ from the intended content, or equal the original, `COULD_NOT_MUTATE`. 6. Run the suite. Non-zero: `CAUGHT`. Zero: `NOT_CAUGHT`. 7. Write the original bytes back. This happens once step 4 has succeeded, whatever the verdict. Step 5 is the [proven-landed guarantee](concepts.md#proven-landed). Steps 2, 3 and 5 are why a `NOT_CAUGHT` always means the fault was in place. Figure: The order of one run. A failure in the first two steps discards the run with exit 2; a mutation that cannot be planted is recorded and the run continues. 1. Baseline: run the suite on pristine source. It must pass, or the run is discarded (exit 2). 2. Positive control: plant, prove on disk, run the suite, restore. It must be CAUGHT, or the run is discarded (exit 2). 3. Each mutation, in spec order: plant, prove on disk, run the suite, restore. A mutation that cannot be planted or proven is COULD_NOT_MUTATE and the suite is not run. 4. Report: per-mutation verdicts in spec order, then the Totals line. Exit 0. ## The suite process The suite is `sh -c ''`, started with its working directory set to the tree's root and its environment set to muhar's own with `GOCACHE` replaced. Its stdout and stderr both go to muhar's stderr. muhar waits for it to exit; only the exit status is used. ## Worker copies With `-j k` above 1, muhar caps the worker count at the number of mutations. If at least two workers remain, it makes one copy of the tree per worker before the baseline, each in a new temp directory named `muhar--*`. With one mutation, it makes no copies and runs in place, even with `-j 2` or `-j 0`. Figure: -j 2: the original tree is copied once per worker. The baseline and control run in copy 0; mutations are dealt to whichever worker is free; results are reported in spec order. Original tree (root): read only during the run, except for absolute file paths. Copy 0 (worker 0): baseline, then control, then mutations as dealt. Copy 1 (worker 1): mutations as dealt. Both workers share the one per-run GOCACHE. The copy is a full deep copy: regular files with their permission bits, directories, and symbolic links recreated as links with the same target text. Sockets, devices and other special files are skipped. Everything under `root` is copied, including `.git`, `node_modules` and build output, so point `root` at the smallest tree the suite needs. After the preflight, mutation indices are fed to the workers one at a time; each worker plants, tests and restores in its own copy. Results are stored by spec index, so the report does not depend on which worker finished first. When the run ends normally, every copy is removed. ### go.mod replace directives A `replace example.com/x => ../x` in `go.mod` is resolved relative to that `go.mod`'s directory. In a copy under the temp directory, a relative target that points outside the copied tree resolves to nothing, every build in the copy fails, and the baseline would discard the run. So, for every `go.mod` in each copy (skipping `testdata` and directories whose names start with `.` or `_`, which the go command ignores), muhar reads the replace directives with `go mod edit -json` and rewrites each one that is a filesystem path, relative, and outside the source tree to the absolute path of its target in the original tree. It then re-reads the directives and fails the copy if any rewrite did not land. Relative targets inside the tree are left alone — they were copied, and resolving them inside the copy keeps mutations to them isolated. Versioned replacements and absolute paths already resolve from anywhere. This uses the `go` command, which must be on `PATH` when any copied `go.mod` contains the word `replace`. A failure to rewrite is a workspace failure, reported as `HARNESS BROKEN`. ## The per-run Go build cache Every mutant is a source tree the Go build cache has never seen, so each suite run adds entries that will never be used again. In a long-lived checkout or CI cache that directory grows without bound; Go trims unused entries only after five days, and that horizon is not configurable. muhar therefore gives the suite a fresh `GOCACHE` for the whole run, shared by every suite run and every worker in it, and removes it at the end. Mutants of one package share most of their build, so one cache per run keeps that speed-up within the run. `GOMODCACHE` is left alone: downloaded modules are immutable and safe to share. The cache is removed on a normal exit, on a broken run, and on `SIGINT` or `SIGTERM`. ## Signals and cleanup On `SIGINT` or `SIGTERM` muhar removes the per-run build cache and exits 130 at once. It does not wait for the suite or run any other clean-up: - In a sequential run (including the one-mutation fallback with `-j` above 1), the file being mutated at that moment is left mutated on disk. Check `git status` and restore it with `git checkout -- ` (or `git restore `) before you commit. - When two or more effective workers are used, the worker copies are left in the temp directory as `muhar--*`. Remove them by hand. - After `SIGKILL`, the build cache is left too, as `assay-mutant-gocache-*`. This is a known gap, tracked as [assay#2641](https://github.com/medici-finance/assay/issues/2641). Until it is fixed, run muhar on a clean working tree, so an interrupted run is obvious in `git status`. ## Timeouts muhar has no timeout of its own. A mutation that makes the suite hang — a loop condition that never becomes false — hangs muhar. Put the limit in the `test` command: - `go test ./... -timeout 60s` fails the test binary when the limit is hit, so the mutation is reported `CAUGHT`. - For any other suite, wrap it: `timeout 120 make test`. GNU `timeout` exits 124 when the limit is hit, which is non-zero, so `CAUGHT`. Set the limit well above the baseline's run time, or a slow machine turns the baseline red and discards the run. A per-mutation timeout is planned ([roadmap](roadmap.md)). ## What a run costs Each invocation runs the suite `2 + M` times: baseline, control, and one per mutation. With `-shard i/n` it is `2 + ⌈M/n⌉` or fewer per shard; every shard pays the 2. With `-j k` the `M` mutation runs proceed `k` at a time, after the `k` copies are made and the preflight has finished. The whole suite runs for every mutation. muhar does not yet select only the tests that reach the mutated code, and every Go mutant rebuilds the mutated package and whatever imports it. Work to reduce both is in progress: build-cost reductions and measured `-j` sizing ([assay#2635](https://github.com/medici-finance/assay/issues/2635)), and coverage-guided test selection ([assay#2636](https://github.com/medici-finance/assay/issues/2636)). Neither is released; see the [roadmap](roadmap.md). --- Source of this page: https://muhar.assay.guide/internals # Writing good mutants A mutant is a claim: "if this line were wrong, a test would fail". Write one for each guard whose silent removal you would not notice in review. muhar is language-agnostic: it edits text and runs a command, so anything a suite can check can be mutated. ## Choosing guards Mutate the lines that refuse things. Good candidates: - Boundary comparisons: `<=` against `<`, `>` against `>=`. - Early returns of an error: `return ErrOverLimit` against `return nil`. - Authorization and ownership checks: a `signatory`, a `require`, an `if !allowed`. - Configuration that turns a protection on: `"enforce": true`, `set -e`, `permissions: read`. - Validation of input at a trust boundary. Skip lines whose failure would be loud anyway (a typo that stops the build), logging, and formatting. A spec of ten guards you care about is worth more than a hundred generated edits you will not read. ## Name the bug, not the edit The `name` is what a reader sees in the report and in CI logs. Describe the behaviour the mutant simulates: | Weak | Strong | |---|---| | `change <= to <` | `zero is accepted as an amount` | | `remove return` | `an over-limit withdrawal is allowed` | | `flip bool` | `branch protection no longer requires review` | A `NOT_CAUGHT` line should read as the bug report for the missing test. ## The control The control proves the harness can see a fault at all. Make it the bluntest edit available in the same file or package the mutations target: disable the main refusal, return success unconditionally, delete the check. It must fail the suite on every run, and it should fail it for the same reason a real regression would — through the tests, not through a build error in an unrelated file. Keep the control in the same tree and build unit as the mutations. A control in another module proves that module is wired up, not this one. ## Old text that survives edits `old` is matched exactly and replaced once ([spec reference](spec.md#matching)). Three habits keep it stable: - Include enough context to be unique — usually the whole condition, not one operator. - Leave out neighbouring lines that change often: comments, log messages, version strings. - When a refactor moves the guard, muhar reports `COULD_NOT_MUTATE`, `Old text not found`. Treat that as a red result and update the spec in the same change. It is the harness telling you the guard moved, not noise. In JSON, write tabs as `\t` and newlines as `\n`. Prefer a single-line `old` where you can: it is easier to read in review and does not depend on line endings. ## Go ```json { "name": "an over-limit withdrawal is allowed", "file": "limits.go", "old": "\t\treturn ErrOverLimit", "new": "\t\treturn nil" } ``` Run with `"test": "go test ./... -count=1 -timeout 60s"`. `-count=1` disables Go's test result cache so every mutant really runs; `-timeout` bounds a mutant that makes a loop spin ([timeouts](internals.md#timeouts)). A mutant that no longer compiles fails `go test` and is reported `CAUGHT`; see the [FAQ](faq.md#non-compiling). ## JSON configuration A configuration file is a guard when a test or a validator reads it. Mutate the value that turns the protection on: ```json { "name": "the rate limit is switched off", "file": "config/limits.json", "old": "\"enforce\": true", "new": "\"enforce\": false" } ``` This is only caught if some test loads `config/limits.json` and asserts on behaviour that depends on it. A `NOT_CAUGHT` here often means the tests use a fixture instead of the shipped file. ## Shell scripts Shell guards are flags and early exits. A test for a script is any command that runs it against a case it must refuse: ```json { "root": ".", "test": "sh tests/run.sh", "control": { "name": "CONTROL: the deploy script never refuses", "file": "scripts/deploy.sh", "old": " echo \"refusing: working tree is dirty\" >&2\n exit 1", "new": " echo \"refusing: working tree is dirty\" >&2" }, "mutations": [ { "name": "a failing step no longer stops the script", "file": "scripts/deploy.sh", "old": "set -eu", "new": "set -u" } ] } ``` `tests/run.sh` might create a temporary repository with an uncommitted file, run `scripts/deploy.sh` in it, and exit non-zero unless the script exited non-zero. The `set -eu` mutant is caught only if some test makes an intermediate step fail and checks the script stops. ## YAML workflows CI workflows carry permissions and conditions that are easy to loosen in review. They are guards when a linter or policy check reads them: ```json { "name": "the workflow token can write to the repository", "file": ".github/workflows/build.yml", "old": " contents: read", "new": " contents: write" } ``` With `"test": "./scripts/check-workflows.sh"`, where that script fails on any workflow granting write access it does not allow. Without such a check, this mutant is `NOT_CAUGHT` by design — and that is the finding. ## One spec per suite A spec has one `test` command. If different guards are covered by different suites — Go tests, shell tests, a workflow linter — write one spec per suite and run each as its own step or CI job. Each one then proves its own baseline and control, and a broken shell suite cannot be hidden by a healthy Go run. ## Reviewing a spec Before merging a spec change, check: - The control is the bluntest available edit in the same build unit as the mutations. - Every `old` occurs in its file, and the first occurrence is the one intended. - Every `name` describes a bug a reader would recognise. - The `test` command has a timeout and does not reuse cached results. - The spec runs green locally: exit 0, `0 NOT CAUGHT, 0 could-not-mutate`. Validate the shape against the [JSON Schema](spec.md#schema) to catch misspelt keys, which muhar's decoder ignores. --- Source of this page: https://muhar.assay.guide/writing-mutants # CI integration A muhar step in CI has three parts: install a pinned muhar, run the spec, and fail unless the exit status is 0 *and* the `Totals:` line shows zero `NOT CAUGHT` and zero `could-not-mutate`. Everything on this page builds on the [gate rule](output.md#gate). ## A single job GitHub Actions, one spec, one runner: ```yaml jobs: mutation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install muhar env: BASE: https://github.com/medici-finance/assay/releases/download/v1.0.39 run: | set -euo pipefail curl -fsSLO "$BASE/desk-tools-linux-amd64.tar.gz" curl -fsSLO "$BASE/checksums.txt" grep " desk-tools-linux-amd64.tar.gz\$" checksums.txt | sha256sum -c - tar -xzf desk-tools-linux-amd64.tar.gz ./muhar install -m 0755 muhar "$RUNNER_TEMP/muhar" - name: Mutation gate shell: bash run: | set -uo pipefail status=0 report=$("$RUNNER_TEMP/muhar" -spec mutations.json) || status=$? printf '%s\n' "$report" if [ "$status" -ne 0 ]; then echo "::error::muhar exited $status: harness broken or usage error, no verdict" exit 1 fi if ! grep -qE '^Totals: [0-9]+ caught, 0 NOT CAUGHT, 0 could-not-mutate\.$' <<<"$report"; then echo "::error::a guard has no test, or a mutation did not land" exit 1 fi ``` Notes: - The step uses `set -uo pipefail` without `-e`, so a non-zero muhar exit is captured in `status` and reported, instead of ending the step before the message. - The suite's output goes to stderr and appears in the log above the report. The report itself is printed once, by `printf`. - `actions/setup-go` is needed only because this example's suite is `go test`. muhar itself needs no Go toolchain, except for the [replace rewrite](internals.md#module-replace) with `-j` above 1. - Pin the release in `BASE`. With `latest`, a new release can change the gate without a commit. ## Splitting across matrix legs A large spec can take longer than one job should. `-shard i/n` runs a disjoint share of the mutations; `n` legs with `i` from `0` to `n-1` cover every mutation exactly once ([CLI reference](cli.md#shard)). Every leg proves the baseline and control on its own, so a broken harness fails all of them. ```yaml jobs: mutation: runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: ["0/3", "1/3", "2/3"] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install muhar run: ./ci/install-muhar.sh - name: Mutation gate, shard ${{ matrix.shard }} shell: bash env: MUHAR_SHARD: ${{ matrix.shard }} run: | set -uo pipefail status=0 report=$(muhar -j 0 -shard "$MUHAR_SHARD" -spec mutations.json) || status=$? printf '%s\n' "$report" mkdir -p "$RUNNER_TEMP/muhar-reports" printf '%s\n' "$report" > "$RUNNER_TEMP/muhar-reports/shard-${MUHAR_SHARD%/*}.txt" if [ "$status" -ne 0 ]; then echo "::error::shard $MUHAR_SHARD: muhar exited $status, no verdict" exit 1 fi grep -qE '^Totals: [0-9]+ caught, 0 NOT CAUGHT, 0 could-not-mutate\.$' <<<"$report" || { echo "::error::shard $MUHAR_SHARD: a guard has no test, or a mutation did not land" exit 1 } - uses: actions/upload-artifact@v4 if: always() with: name: muhar-shard-${{ strategy.job-index }} path: ${{ runner.temp }}/muhar-reports/ ``` `./ci/install-muhar.sh` stands for the install step from the single-job example; it must put `muhar` on `PATH`. - Pass the shard through an environment variable rather than interpolating `${{ matrix.shard }}` into the script. It keeps the expression out of the shell text. - `fail-fast: false` lets every leg finish, so one run shows every missing test, not the first shard's. - The matrix list must name every `i/n` for one `n`. A missing leg silently drops its mutations from the gate. Generate the list, or keep it next to a comment stating `n`. - Each leg's `Totals:` line covers that leg only. The gate is the conjunction: every leg green. - Every leg pays for the baseline and the control. Shard when the mutations dominate the run time, not for a spec of three. ## Sizing -j `-j 0` uses one worker per CPU, capped at 2, which bounds peak memory on large runners ([why](cli.md#j-cap)). On a runner with headroom, an explicit `-j 4` may be faster; measure it, because each worker holds a full copy of `root` and its own suite process. Combine `-j` with `-shard`: shard across machines, `-j` within one. Point `root` at the smallest tree the suite needs. Every worker copies everything under it, including `.git` and `node_modules`. ## What to keep Keep the report — stdout — as a build artifact, as in the matrix example. The suite's output on stderr is in the job log. A `COULD_NOT_MUTATE` line names the mutation and why it did not land, so the report alone is enough to fix the spec. ## Other CI systems The gate is plain bash, so it carries over unchanged. In GitLab CI: ```yaml mutation: image: golang:1.25 parallel: matrix: - MUHAR_SHARD: ["0/3", "1/3", "2/3"] script: - ./ci/install-muhar.sh - ./ci/muhar-gate.sh ``` where `ci/muhar-gate.sh` holds the bash gate, reading `MUHAR_SHARD`. Any system that runs a script and fails on a non-zero exit can run it. ## How Assay uses it Assay's release workflow runs muhar on its own merge tool: three matrix legs, `-shard 0/3`, `1/3` and `2/3`, each `go run ./cmd/muhar -j 0 -shard "$MUHAR_SHARD" -spec cmd/deskmerge/mutations.json` from the `tools/desk` module, gated on the same `Totals:` pattern. A second leg runs six smaller specs one after another with `-j 0`. Reports are written to the runner's temp directory. ## Coming A reusable CI action that installs muhar, runs a sharded sweep and applies this gate is planned ([assay#2642](https://github.com/medici-finance/assay/issues/2642)), as is a diff-scoped mode that runs only the mutations touching changed files ([assay#2640](https://github.com/medici-finance/assay/issues/2640)). Neither exists yet; see the [roadmap](roadmap.md). --- Source of this page: https://muhar.assay.guide/ci # Comparison muhar is not a replacement for a mutation-testing framework. The established tools generate mutants automatically across a whole codebase and report a mutation score; muhar runs a short list of hand-written mutants and proves each one. Most projects that want broad mutation coverage should use the framework for their language. muhar fits next to it, or on its own where the guards matter more than the score. The descriptions of other tools below are summaries of their public documentation as of October 2026. Check each tool's own documentation for current behaviour. ## At a glance | Tool | Languages | Mutants | Test selection | Statuses beyond caught and missed | |---|---|---|---|---| | muhar | Any text the suite reads | Hand-written, exact text edits | Whole suite per mutant | `COULD_NOT_MUTATE`; run-level `HARNESS BROKEN` | | [Gremlins](https://gremlins.dev/) | Go | Generated by operator | Coverage-based | not covered, timed out, not viable | | [go-mutesting](https://github.com/zimmski/go-mutesting) | Go | Generated by mutator | Configurable through an exec script | skipped | | [Stryker](https://stryker-mutator.io/) | JavaScript, TypeScript, C#, Scala | Generated, as mutant schemata | Per-test coverage | no coverage, timeout, compile error, runtime error, ignored | | [PIT](https://pitest.org/) | JVM languages | Generated, on bytecode | Coverage-guided per mutant | no coverage, timed out, memory error, run error, non-viable | | [mutmut](https://mutmut.readthedocs.io/) | Python | Generated | Per-function test selection | timeout, suspicious, skipped | | [cargo-mutants](https://mutants.rs/) | Rust | Generated, at function and operator level | Tests of the mutated package by default, configurable | unviable, timeout | ## What muhar does differently - **Every mutant is a named guard.** There is no generated list to triage and no score; each report line answers one question you chose. - **Each edit is proven on disk.** muhar reads the file back after writing it and reports `COULD_NOT_MUTATE` if the bytes are not the intended mutation ([proven-landed](concepts.md#proven-landed)). A mutant that never landed cannot be counted as killed or survived. - **A broken harness cannot read as clean.** A mandatory [positive control](concepts.md#control) must be caught before any verdict is reported. A red baseline or a missed control discards the run with exit 2 and prints no `Totals:` line ([exit codes](output.md#exit-codes)). - **Language-agnostic.** muhar edits text and runs a command, so the same spec format covers Go, configuration, shell and workflow files ([writing good mutants](writing-mutants.md)). ## What muhar does worse - **No mutant generation.** You write every mutant. muhar will not find the guard you forgot to list. - **No mutation score.** There is nothing to trend or threshold across a codebase. - **No coverage-guided selection.** Every mutant runs the whole suite. Planned: [assay#2636](https://github.com/medici-finance/assay/issues/2636). - **No per-mutant timeout.** Put the limit in the `test` command ([timeouts](internals.md#timeouts)). - **Non-compiling mutants count as caught.** A typo in `new` that breaks the build is reported `CAUGHT`, not unviable. Planned: [assay#2639](https://github.com/medici-finance/assay/issues/2639). - **No incremental or diff mode.** Every run starts from scratch. Planned: [assay#2640](https://github.com/medici-finance/assay/issues/2640). - **Text report only.** No HTML, no JSON output. - **Go-specific conveniences only.** The per-run build cache and `go.mod` rewriting help Go trees; other ecosystems get no equivalent. ## Gremlins [Gremlins](https://gremlins.dev/) is a mutation-testing tool for Go. It finds candidate mutation points with Go's coverage tooling, applies its operators (arithmetic, conditionals, increments, inversions), and reports each mutant as killed, lived, not covered, timed out or not viable, with efficacy and mutant-coverage figures. Thresholds on those figures make the run exit non-zero. **Gremlins is better** when you want broad, automatic coverage of a Go module, a trend figure to watch, and to skip uncovered code without running it. It also understands Go syntax, so its mutants are always well-formed. **muhar is the choice** when you need a specific list of guards proven with a verdict each, a control that discards a harness that is not mutating, or mutants outside Go source. ## go-mutesting [go-mutesting](https://github.com/zimmski/go-mutesting) generates mutants of Go source with a set of mutators (branch removal, expression and statement mutators) and reports a mutation score. An exec script hook lets you control how each mutant is tested. The original repository has seen little recent maintenance; several forks continue it. **go-mutesting is better** when you want generated Go mutants with a scriptable test step. **muhar is the choice** when you want named, reviewed mutants, proof that each one landed, and a harness-health check before any verdict. ## Stryker [Stryker](https://stryker-mutator.io/) covers JavaScript and TypeScript (StrykerJS), C# (Stryker.NET) and Scala (Stryker4s). It applies many mutants at once through mutant schemata, measures per-test coverage to run only the tests that reach each mutant, supports incremental runs, and produces an HTML report. It classifies mutants as killed, survived, no coverage, timeout, compile error, runtime error or ignored, and thresholds (`high`, `low`, `break`) control the exit status. **Stryker is better** at almost everything a framework does: speed through schemata and test selection, reporting, incremental runs, and ecosystem integration for its languages. **muhar is the choice** for named guards and for files outside Stryker's languages. ## PIT [PIT](https://pitest.org/) mutates JVM bytecode, which makes it fast, and runs only the tests that cover each mutant. Its statuses are killed, survived, no coverage, timed out, memory error, run error and non-viable. It supports mutation and coverage thresholds, incremental analysis from history files, and integrates with Maven and Gradle. **PIT is better** for any JVM codebase: its coverage-guided selection and bytecode mutation make whole-codebase runs practical, and its reports are mature. **muhar is the choice** for a short list of guards outside the JVM, or when the evidence that each fault landed matters more than the count. ## mutmut [mutmut](https://mutmut.readthedocs.io/) generates mutants of Python code, runs the tests relevant to each mutated function, and keeps its state so you can browse and apply surviving mutants afterwards. **mutmut is better** for Python: generated mutants, test selection, and an interactive workflow for reviewing survivors. **muhar is the choice** for named guards in any language, with a run-level health check. ## cargo-mutants [cargo-mutants](https://mutants.rs/) is the closest in spirit to muhar. It generates mutants of Rust code (replacing function bodies, swapping operators), copies the tree to a scratch directory so the source is never touched, checks that the unmutated tree builds and passes first, and reports each mutant as caught, missed, unviable or timeout. Its exit status distinguishes missed mutants, timeouts and a failing baseline. It supports `--shard k/n`, `--in-diff` for changed code only, and `--jobs` for parallel builds. **cargo-mutants is better** for Rust: generated mutants, a separate unviable status, timeouts, diff-scoped runs and a mature output directory. **muhar is the choice** outside Rust, for a mandatory positive control, and for hand-chosen guards with a verdict each. ## When to use which | You want | Use | |---|---| | A mutation score for a whole JVM, JS/TS, .NET, Scala, Python or Rust codebase | That language's framework: PIT, Stryker, mutmut, cargo-mutants | | Generated mutants across a Go module | Gremlins or a maintained go-mutesting fork | | A short list of guards proven in CI, with a verdict per guard | muhar | | Mutants in configuration, shell or workflow files | muhar | | Both | A framework for breadth, muhar for the guards you name | --- Source of this page: https://muhar.assay.guide/comparison # FAQ ## Why did my run exit 2 when the tests pass? Exit 2 is `HARNESS BROKEN`. Read the reason on stdout. The two common causes: the `test` command fails on pristine source in `root` (often a wrong `root`, or a command that only works from another directory), or the control's `old` text no longer matches the file. Run the `test` command by hand from `root` first. If stdout is empty, the 2 came from Go's flag parser: check the flags ([exit codes](output.md#exit-codes)). ## Is a COULD_NOT_MUTATE line a failure? Treat it as one. The guard it names was never tested in this run, so the run says nothing about it. The usual cause is drift: the code changed and the spec's `old` text did not. Update the spec in the same change as the code. muhar prints `NOTE: could-not-mutate is NOT a clean gate` whenever one appears. ## What happens to a mutant that does not compile? It fails the suite, so muhar reports it `CAUGHT`. For Go, a typo in `new` therefore looks like a caught mutant. Check that each `new` compiles when you write it — temporarily apply it and build. A separate unviable verdict is planned ([assay#2639](https://github.com/medici-finance/assay/issues/2639)). ## I pressed Ctrl-C. Is my source tree clean? Not necessarily. An interrupt exits 130 straight away, without restoring the file under test in a sequential run, and without removing worker copies with `-j` above 1. Run `git status` and restore any mutated file; remove leftover `muhar-*` directories from the temp directory. Details and the tracking issue: [Signals and cleanup](internals.md#signals). ## Does muhar work outside Go? Yes. muhar edits text and runs any `sh -c` command. The examples on [Writing good mutants](writing-mutants.md) cover JSON, shell and YAML. The Go-specific parts — the per-run build cache and the `go.mod` rewrite — only take effect for Go trees and are harmless elsewhere. ## Why is -j 0 only 2 workers on my 32-core runner? Each worker holds a copy of the tree and runs its own suite, so memory grows with workers. The cap keeps `-j 0` safe on runners whose memory does not scale with cores. Pass an explicit `-j` to go higher ([CLI reference](cli.md#j-cap)). ## Can I use absolute file paths? With `-j 1`, yes. With `-j` above 1 or `-j 0`, no: an absolute path points at the original tree, not the worker's copy, and produces wrong verdicts ([spec reference](spec.md#absolute-paths)). ## Can mutations depend on each other? No. Each mutation is planted on pristine source and restored before the next, so every verdict is about one edit alone. To test a combination, write it as one mutation whose `old` and `new` span both changes, if they are adjacent, or as a separate spec. ## How do I run only the mutations for my change? Not yet. Today every mutation in the spec runs. A diff-scoped mode is planned ([assay#2640](https://github.com/medici-finance/assay/issues/2640)). Meanwhile, split specs by package and run the ones your change touches. ## How do AI agents use this site? Every page has a plain-Markdown twin at the same path with `.md` in place of `.html`, linked from the page head and footer. [llms.txt](llms.txt) lists every page with a one-line description; [llms-full.txt](llms-full.txt) is the whole site as one Markdown file. The spec format is published as a [JSON Schema](schema/spec.v1.json). Every flag, spec field, verdict and exit code has a stable anchor, so links such as `cli.html#flag-shard` and `spec.html#field-old` will keep working. ## How should an agent read a muhar result? Check the exit status first: anything but 0 means no verdict. Then parse the `Totals:` line and each verdict line with the patterns on [Output and exit codes](output.md#parsing). Report a `NOT_CAUGHT` line as a missing test for the named guard, and a `COULD_NOT_MUTATE` line as a spec that needs updating — never either as a pass. ## Where do I report a bug? In the [medici-finance/assay issue tracker](https://github.com/medici-finance/assay/issues). Include the muhar command, the spec, the exit status and stdout. --- Source of this page: https://muhar.assay.guide/faq # Roadmap Everything on this page is coming, not shipped. Each item is an open issue in the [medici-finance/assay](https://github.com/medici-finance/assay/issues) tracker; follow the link for its current state. The rest of this site documents only what released muhar does today. ## Speed | Item | Issue | |---|---| | Cut the build cost of each mutant: overlay the mutated file instead of copying the tree, share the build cache across workers safely, and size `-j` from measurements rather than a fixed cap. | [assay#2635](https://github.com/medici-finance/assay/issues/2635) | | Coverage-guided test selection: run only the tests that reach the mutated code, instead of the whole suite per mutant. | [assay#2636](https://github.com/medici-finance/assay/issues/2636) | | A diff-scoped mode, `-diff`, that runs only the mutations whose files a change touches. | [assay#2640](https://github.com/medici-finance/assay/issues/2640) | ## Install and CI | Item | Issue | |---|---| | A standalone muhar README and a one-line install that fetches muhar alone, not the whole desk-tools bundle. | [assay#2638](https://github.com/medici-finance/assay/issues/2638) | | A reusable CI composite action: install a pinned muhar, run a sharded sweep, apply the `Totals:` gate. | [assay#2642](https://github.com/medici-finance/assay/issues/2642) | Until these land, use the [release bundle](install.md#release) and the [CI recipes](ci.md). ## Verdicts | Item | Issue | |---|---| | Report a mutant that does not compile as unviable instead of `CAUGHT`, and add a per-mutant timeout. | [assay#2639](https://github.com/medici-finance/assay/issues/2639) | ## Known issues These are defects in released muhar. Each page that is affected says so where it matters. | Issue | Effect today | Workaround | |---|---|---| | [assay#2641](https://github.com/medici-finance/assay/issues/2641) | `SIGINT` or `SIGTERM` exits 130 without restoring the file under test (sequential runs) or removing worker copies (`-j` above 1). | Run on a clean tree; after an interrupt, check `git status` and the temp directory ([signals](internals.md#signals)). | | [assay#2643](https://github.com/medici-finance/assay/issues/2643) | An absolute `file` with `-j` above 1 edits the original tree while the suite runs in a copy, giving false `NOT_CAUGHT` verdicts. | Use relative `file` paths ([spec reference](spec.md#absolute-paths)). | | [assay#2644](https://github.com/medici-finance/assay/issues/2644) | A flag-parse error exits 2, the same status as `HARNESS BROKEN`. | Fail on any non-zero exit; a flag error prints nothing on stdout ([exit codes](output.md#exit-codes)). | | [assay#1382](https://github.com/medici-finance/assay/issues/1382) | With `-j 4`, a spec has been seen to report false `COULD_NOT_MUTATE` for entries that `-j 1` plants and catches. The cause is not fully diagnosed; the worker copies appear to lack files the spec names. | Treat a `COULD_NOT_MUTATE` from a parallel run as suspect and confirm it with `-j 1`; keep every file the spec names and the suite reads under `root`. | --- Source of this page: https://muhar.assay.guide/roadmap