Skip to content

fix(quality): count mutation timeouts as killed up to a cap - #233

Merged
tbhb-agent merged 9 commits into
mainfrom
agent/rules-mutation-timeouts
Nov 14, 2026
Merged

tbhb-agent merged 9 commits into
mainfrom
agent/rules-mutation-timeouts

Conversation

@tbhb-agent

@tbhb-agent tbhb-agent commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Why

A starved CPU makes tests time out, and the strict gate fails the whole run on any timeout. One real run on 2 CPUs timed out 212 of 849 mutants. @tbhb decided that timeouts count as killed, with a cap so starvation can't pass as kills.

Behavior change

mutation_gate_test no longer fails on a timed-out mutant. It lists each one and prints mutation_gate: N timed out of M (X%), counted as killed, to $GITHUB_STEP_SUMMARY too when set. It fails when timeouts exceed 10% of the mutants, in the sum, and in each report. Exactly 10% passes. Survivors, uncovered mutants, and errored mutants still fail. The gate rejects counts past 10^9, counts that don't add up to the total, and listed verdicts that contradict the counts.

The mutmut and gomutants runners write their report, then exit non-zero only past the cap. Within the cap they exit 0 and Bazel caches the pass, as the loosening decision means.

Decisions

  • The gate and both runners read one shared cap constant. The macros have no cap attribute.
  • The gate checks each report against the cap alone, so one report with many timeouts can't hide behind clean ones.
  • Within-cap reports stay cached passes, and past-cap reports fail. If that needs to change, undo it by failing on any timeout or tagging timeout-prone units no-cache.
  • The count line uses integer math, rounded down to a tenth of a percent.
  • A report with timeouts and a zero total fails through the existing unchecked-mutants check.
  • Per-pattern suppressions, an advisory pull request lane and a blocking nightly run are separate follow-ups.

Risk

A run with up to 10% timeouts now passes where it failed. The cap bounds that, and the report still lists every timeout.

Bazel caches a starved run within the cap as a pass and replays it later. It also caches full-scope runs when every report is empty, since the runner exits 0. Only the gate rejects that. It also enforces the 10^9 count cap. Runners skip that check, and they read +5 as 5.

Verification

None beyond CI.

Opened-by: LLM effort=medium;harness=claude-code:2.1.322;model=anthropic:claude-sonnet-5-5;session=sha-256-64:debb814921f029ad;surface=cli
Assisted-by: LLM effort=medium;harness=claude-code:2.1.322;model=anthropic:claude-sonnet-5-5;session=sha-256-64:debb814921f029ad;surface=cli

A timed-out mutant now counts as killed and stays listed. The gate
prints a count line (and appends it to the job summary when
GITHUB_STEP_SUMMARY is set) and fails when timeouts exceed 10% of the
mutants, so CPU starvation cannot pass as kills. Survivors still fail.

Both runners no longer fail their own action on a timeout, so the
report reaches the gate.

Generated-by: LLM effort=medium;harness=claude-code:2.1.322;model=anthropic:claude-sonnet-5-5;session=sha-256-64:8786680e37c864fa;surface=sdk-cli
Generated-by: LLM effort=medium;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:8786680e37c864fa;surface=sdk-cli
Generated-by: LLM effort=medium;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:fd164b5936a0fa78;surface=sdk-cli
A run that is both errored and interrupted prints both verdicts in the
not-cacheable message. With timeout no longer unstable, no other test
listed two verdicts, so a mutant of the join separator survived.

Generated-by: LLM effort=medium;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:ab53f8c879532bcb;surface=sdk-cli
Both runners now exit non-zero when timeouts exceed
MUTATION_MAX_TIMEOUT_PERCENT, so Bazel does not cache a starved run. The
gate reads the same constant, applies it to each report as well as the
sum, rejects counts above 10^9, and requires exact per-report accounting
with listed verdict lines matching the counts.

Generated-by: LLM effort=high;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:29dbf9027bd7e66c;surface=sdk-cli
The count-bound check compares equal-length digit strings as text on
purpose, and the per-report counts are set through eval, so shellcheck
flagged SC2071, SC2034 and SC2154. Mark each with its reason and move
the bound test out of the if condition so the directive can attach.

Generated-by: LLM effort=high;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:29dbf9027bd7e66c;surface=sdk-cli
The gate now fails a report with a mutant.not_checked line even when
the counts balance, and a mutant.<status> line whose status no verdict
names.

The mutmut runner records a failed `mutmut run` or `mutmut results` as
interrupted, and runs the gate's accounting checks before returning
success: counts that don't add up to the total, a mutant listed under a
verdict its count denies, an unknown status, an unmapped mutant. It
also bounds the timeout cap's length before converting it.

The gomutants runner exits non-zero for a diff report whose selected
mutants are not the total, or that lists an unmapped mutant, after
writing the report.

Bazel does not cache a failed action, so each of these runs is rerun
instead of replayed.

Generated-by: LLM effort=high;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:75b262e3e7e8f983;surface=sdk-cli
The report's consistency check reads a mutant line's status before the
first equals sign. A mutant name can hold one, and mutmut's
partition-to-rpartition change survived the self mutation gate. Test a
known and an unknown status with an equals sign in the mutant name.

Generated-by: LLM effort=high;harness=claude-code:2.1.325;model=anthropic:claude-sonnet-5-5;session=sha-256-64:75b262e3e7e8f983;surface=sdk-cli
Generated-by: LLM effort=medium;harness=claude-code:2.1.318;model=anthropic:claude-sonnet-5-5;session=sha-256-64:261946672f684d58;surface=sdk-cli
@tbhb-agent
tbhb-agent merged commit d3b22ae into main Nov 14, 2026
104 checks passed
@tbhb-agent
tbhb-agent deleted the agent/rules-mutation-timeouts branch November 14, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant