Skip to content

test(semantic): close the mutation-testing gaps in the decision core - #490

Merged
kevincostner17 merged 1 commit into
mainfrom
test/mutation-core
Sep 20, 2026
Merged

kevincostner17 merged 1 commit into
mainfrom
test/mutation-core

Conversation

@kevincostner17

Copy link
Copy Markdown
Contributor

Why

This is the measurement the programme was missing: 7120 green tests, branch = false, and no mutant had ever been killed. Line coverage of 94.96% says nothing about whether the assertions would catch a defect.

Whole-suite mutation is infeasible at this size, so this scopes it to the two modules where a surviving mutant is most alarming — semantic/policy.py (the gate deciding whether a repair is applied) and guard.py (protected-column byte-identity). 19 targeted mutants covering comparison flips, boolean-operator swaps and constant flips.

Result

mutants killed survived kill rate
Before 19 12 7 63.2%
After 19 18 1 94.7%

All seven original survivors were re-run against the entire 7120-test suite and survived that too — so these were genuine gaps, not artefacts of a targeted subset.

What was untested

Four of the five policy survivors sat in the safety core:

mutant effect
policy bool#4 bool(raw_payload) and raw_payload == payload(proposed) → or. Any non-empty payload passes, so a proposal that changes an identifier's content slips through the carve-out — exactly what that docstring says it prevents
policy bool#8 target-column protection or → and: lost unless both declared name and inferred role match
policy bool#12 preserve_columns protection or → and: same
policy bool#1, const_bool#2 the non-string early return in _payload_preserving
guard const_bool#4 protected snapshot copy(deep=True) → deep=False. The snapshot verify_protected compares against would share the frame's buffer, so an in-place edit would change the snapshot too and the violation would go unnoticed

None of these were bugs in shipped behaviour — the code is correct. Nothing was checking that it stays correct. The 18 tests here do.

The remaining survivor is equivalent, and that is proven

guard bool#5 (if left_na or right_na → and) changes only the one-sided-missing branch of _cell_equal, where the fall-through comparison returns False anyway — the same answer the early return gives.

Both versions were run over 289 input pairs (None, nan, NaT, pd.NA, ints, strings, bools, lists, dicts, ndarray, inf, bytes): zero behavioural differences. It cannot be killed because it does not change behaviour, so a test chasing it would assert nothing. Documented in the test file so nobody re-derives that.

Effective kill rate on non-equivalent mutants: 18/18.

Scope, stated plainly

Two modules, not the codebase. scoring.py, experts.py, steps/dtypes.py and fieldcheck.py remain unmeasured, and branch coverage is still false repo-wide. This establishes that the decision gate and the protection guard are genuinely tested — not that the other 7100 tests are.

Verification

  • Full suite py3.12: 7138 passed, 20 skipped, 0 failed, coverage 94.96%.
  • ruff check . clean repo-wide.
  • No library code changed.

Whole-suite mutation is infeasible at ~7100 tests, so this scopes it to the two
modules where a surviving mutant is most alarming: semantic/policy.py, the gate
that decides whether a repair is applied, and guard.py, which enforces
protected-column byte-identity. 19 targeted mutants covering the classes the
brief names -- comparison flips, boolean-operator swaps, constant flips.

    before   19 mutants, 12 killed,  7 survived   63.2%
    after    19 mutants, 18 killed,  1 survived   94.7%

All seven original survivors were re-run against the entire 7120-test suite and
survived it too, so these were genuine gaps rather than artefacts of a targeted
subset. 94.96% line coverage was hiding them.

Four of the five policy survivors sat in the safety core:

  policy bool#4    `bool(raw_payload) and raw_payload == payload(proposed)`
                   flipped to `or`: any non-empty payload passes, so a proposal
                   that CHANGES an identifier's content slips through the
                   carve-out -- exactly what the docstring says it prevents.
  policy bool#8    target-column protection `or` -> `and`: protection lost
                   unless both the declared name and the inferred role match.
  policy bool#12   preserve_columns protection `or` -> `and`: same.
  policy bool#1
  const_bool#2     the non-string early return in _payload_preserving, so a
                   non-string pair reaches the payload comparison.
  guard const_bool#4
                   protected snapshot copy(deep=True) -> deep=False: the
                   snapshot verify_protected compares against would share the
                   frame's buffer, so an in-place edit would change the snapshot
                   too and the violation would go unnoticed.

None of these were bugs in shipped behaviour -- the code is correct. Nothing was
checking that it stays correct. The 18 tests here do.

The remaining survivor, guard bool#5 (`if left_na or right_na` -> `and`), is an
equivalent mutant and is documented as such rather than chased. It changes only
the one-sided-missing branch of _cell_equal, where the fall-through comparison
returns False anyway -- the same answer the early return gives. Both versions
were run over 289 input pairs (None, nan, NaT, pd.NA, ints, strings, bools,
lists, dicts, ndarray, inf, bytes): zero behavioural differences. A test written
to chase it would assert nothing.

Scope stated plainly: two modules, not the codebase. scoring.py, experts.py,
steps/dtypes.py and fieldcheck.py remain unmeasured and branch coverage is still
false repo-wide. This establishes that the decision gate and the protection
guard are genuinely tested, not that the other 7100 tests are.

Full suite 7138 passed / 0 failed, coverage 94.96%; ruff clean repo-wide.
No library code changed.
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 94c7c23e-c099-4131-b7ba-582165970fe8


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kevincostner17
kevincostner17 merged commit e7c5247 into main Sep 20, 2026
19 checks passed
kevincostner17 added a commit that referenced this pull request Sep 20, 2026
) (#494)

PR #490 reported "19 mutants, 94.7%" for policy.py + guard.py. That was
measured over 19 of the module pair's 62 mutation sites, because of an
indexing defect in the mutation harness, so it described roughly a third
of the module rather than the module. The true figure was 69.4% with 19
survivors. This adds 30 tests and takes it to 62/62 with no survivors.

The survivors included the most safety-critical mutants found anywhere in
this work:

  is_id = column in ctx.id_columns or (info is not None and info.identifier_like)

flipped to `and` narrows identifier protection so a column detected as
identifier-like, but not explicitly listed, silently loses it. The gate's
own review-threshold comparison was movable. `format_alignment and
_payload_preserving` flipped to `or` would let a non-payload-preserving
repair through. In guard.py the row-count check was movable, and the NA
comparison could be flipped so the byte-identity guard misses a violation.

It also corrects a false claim this file shipped in #490. The docstring
asserted that flipping `if left_na or right_na` to `and` was an equivalent
mutant, "verified across 289 input pairs ... zero behavioural differences.
It cannot be killed." It can. The pool held only ordinary scalars and no
cell whose __eq__ returns True against anything; for such a cell the
fall-through comparison returns True, so the mutant reports a missing value
and a present one as equal and the final identifier-protection layer misses
the violation. A 1296-pair differential finds 18 such disagreements. The
mutant is now killed and the docstring says what went wrong.

That note also cited mutant ids, which are positional and did not survive
the harness fix: its `bool#5` is today's `bool#2`. The corrected docstring
quotes the source line instead.

Tests only; no src change.
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