Mutation: order covering tests cheapest-first in the mutation child ## What When a mutation child runs the tests that cover a mutation, it runs them synchronously under fail-fast, so the first covering test that fails kills the mutant. This orders those covering tests **cheapest-first**, using the per-test baseline timing the coverage phase already measures (and, until now, discarded into a single timeout). Reaching a killing test sooner cuts the mutation phase's wall-clock, since killed mutants are the overwhelming majority of a run. The idea: to minimise expected time to the first success among independent trials with cost `c_i` and (unknown, assumed uniform) kill-probability, order by ascending `c_i`. Shortest-first. ## How - **`reorderTestForestByTiming`** (`sydtest/.../Mutation/Forest.hs`): a pure, tree-aware, stable reorder. It sorts siblings by ascending baseline (missing = first) and orders each subtree by the minimum cost of any leaf it contains, so a shared-setup group reorders as a unit and setup is never split. It mirrors `randomiseTestForest`: inside a `DoNotRandomiseExecutionOrder` scope it leaves the subtree untouched at every depth. So it is a drop-in alternative to execution-order randomisation, legal under exactly the same conditions. - **Baselines persisted**: the coverage phase now writes the merged per-test timings as `baseline.json` next to `manifest-augmented.json`; the `run` and `diff` paths union them into the augmented dir the children read. - **The child** (`MutationMode/Single.hs`) reads that baseline and reorders the filtered forest **after filtering** (so the coverage-order `TestId`s the filter needs are untouched), gated on `settingRandomiseExecutionOrder` (if the author fixed the order with `--no-randomise-execution-order`, we respect it). Only sibling order changes, so verdicts are unaffected: the filter still decides which tests run. ## Results Full mutation check run on two real projects, without vs with this commit, on the same machine. Fair mutation-phase-only comparison: instrumented suites + coverage warmed untimed (they are cachix-cached in real CI), so only the mutation phase is timed. **Verdicts identical in both** (same killed/survived/uncovered, every per-library score line matches). | project | covering-test times (min / p90 / max) | without | with | speedup | |---|---|---|---|---| | nix-ci | 0.2ms / — / 0.7s | 1814s | 1704s | ~6% | | really-safe-money | 0ms / 47ms / 35.8s | 530s | 405s | ~24% | The win scales with the covering tests' time spread: cheapest-first pays off when a mutant's covering set mixes cheap tests with an expensive outlier (killing on a cheap test first avoids ever running the slow one). Projects with a heavy-tailed test-time distribution benefit most. Caveat: n=1 per project, so treat the exact percentages as indicative (the 24% is well above scheduling noise; the 6% is closer to it). The ceiling is set by kill density and per-mutant process overhead, neither of which this change touches. ## Testing - New pure unit tests in `sydtest-mutation` (`ReorderSpec`) covering ascending order, stable ties + missing-as-first, that a `Sequential` (parallelism) subtree is reordered but a `DoNotRandomiseExecutionOrder` one is not (incl. no descent into a nested randomise block), that a `beforeAll_` group reorders as a unit without splitting setup, and that the test-id set is unchanged. Confirmed non-vacuous by disabling the sort and watching them fail. - `nix flake check` passes. - End-to-end verdict-unchanged confirmed by the two mutation runs above. </content>