Close every mutation gap, then gate the full run #6

Merged
cyplo merged 6 commits from close-mutation-gaps into main 2026-11-09 10:29:15 +00:00
Owner

The full mutation run on main (run 17) left 66 missed mutants. This PR closes all of them: new fixtures and tests catch 64, the list-renderer restructure deletes the function holding 15 of them, and one provably equivalent mutant is excluded with its reason in .cargo/mutants.toml. With nothing left, the all-code CI job stops reporting for information and starts gating.

The fixes the mutants exposed

  • The confirm view listed pending changes in hash-map order - 5 runs, 5 orders. It now walks the conflict list; a fixture with one change of every kind pins it.
  • Every key the app reacts to is now tested through the same event loop as the terminal, including q in both views (the list-view quit used to hang mutants into timeouts rather than fail them).
  • Expanding a diff taller than the screen blanked the whole list. Rendering wrote groups at a running row and stopped at the bottom edge, so an over-tall group wrote nothing at all. Groups are now built as lines first and clipped at the edge, so a long diff shows clipped instead of vanishing. Scrolling follows what was drawn.
  • Suggestions were computed but never shown: App::new pre-selected Do nothing for every group, and the Suggested: line only renders when nothing is chosen - so it never rendered. Groups with a suggestion now start unchosen and show it; groups without one keep the Do nothing safe default. Four fixtures pin the scanner's advice from both EXIF sides, equal EXIF dates, an mtime gap exactly at the 1-hour threshold, and one below it.
  • Applying resolutions could leave the selection past the end of the list; a fixture pins it landing back on the surviving group.
  • Without a terminal, the binary lists conflicts - two CLI tests pin that output and that it touches no files.
  • Only jpg/png names get image treatment: JPEG bytes under .md names or with no extension at all stay text (and their EXIF disagrees with their mtimes, so treating them as images would flip the suggestion - the fixtures catch it). A BMP stays plain the same way.

Details worth knowing

  • The crafted fixture JPEGs declared 12-bit SOF precision, which the image crate refuses (it only parses 8-bit); they now declare 8-bit, so their dimensions parse and they show the expand arrow.
  • image is built with only the jpeg and png features, so image_dimensions() can never succeed for any other extension - exactly the set is_image() accepts. Its -> true mutant is therefore equivalent, and excluded in .cargo/mutants.toml with this reason.
  • Fixture mtimes are set from one shared base time, so "exactly one hour apart" is exact rather than a race with SystemTime::now() per file.

Checked

  • 66/66 tests pass, clippy clean with -D warnings.
  • Mutating this PR's whole diff against main, as CI will: 40 mutants, 39 caught, 1 unviable, 0 missed.
  • Every group from run 17 re-run individually: all caught (the per-group numbers are in the job logs of the local runs).
  • forgejo-runner validate accepts the workflow change.

Merging this makes main's full mutation run gating; the next full run after merge should be clean, since every local run is.

The full mutation run on `main` (run 17) left **66 missed mutants**. This PR closes all of them: new fixtures and tests catch 64, the list-renderer restructure deletes the function holding 15 of them, and one provably equivalent mutant is excluded with its reason in `.cargo/mutants.toml`. With nothing left, the all-code CI job stops reporting for information and starts gating. ### The fixes the mutants exposed - **The confirm view listed pending changes in hash-map order** - 5 runs, 5 orders. It now walks the conflict list; a fixture with one change of every kind pins it. - **Every key the app reacts to is now tested** through the same event loop as the terminal, including `q` in both views (the list-view quit used to hang mutants into timeouts rather than fail them). - **Expanding a diff taller than the screen blanked the whole list.** Rendering wrote groups at a running row and stopped at the bottom edge, so an over-tall group wrote nothing at all. Groups are now built as lines first and clipped at the edge, so a long diff shows clipped instead of vanishing. Scrolling follows what was drawn. - **Suggestions were computed but never shown**: `App::new` pre-selected Do nothing for every group, and the `Suggested:` line only renders when nothing is chosen - so it never rendered. Groups with a suggestion now start unchosen and show it; groups without one keep the Do nothing safe default. Four fixtures pin the scanner's advice from both EXIF sides, equal EXIF dates, an mtime gap exactly at the 1-hour threshold, and one below it. - **Applying resolutions could leave the selection past the end of the list**; a fixture pins it landing back on the surviving group. - **Without a terminal, the binary lists conflicts** - two CLI tests pin that output and that it touches no files. - **Only jpg/png names get image treatment**: JPEG bytes under `.md` names or with no extension at all stay text (and their EXIF disagrees with their mtimes, so treating them as images would flip the suggestion - the fixtures catch it). A BMP stays plain the same way. ### Details worth knowing - The crafted fixture JPEGs declared 12-bit SOF precision, which the `image` crate refuses (it only parses 8-bit); they now declare 8-bit, so their dimensions parse and they show the expand arrow. - `image` is built with only the `jpeg` and `png` features, so `image_dimensions()` can never succeed for any other extension - exactly the set `is_image()` accepts. Its `-> true` mutant is therefore equivalent, and excluded in `.cargo/mutants.toml` with this reason. - Fixture mtimes are set from one shared base time, so "exactly one hour apart" is exact rather than a race with `SystemTime::now()` per file. ### Checked - 66/66 tests pass, clippy clean with `-D warnings`. - Mutating this PR's whole diff against `main`, as CI will: **40 mutants, 39 caught, 1 unviable, 0 missed**. - Every group from run 17 re-run individually: all caught (the per-group numbers are in the job logs of the local runs). - `forgejo-runner validate` accepts the workflow change. Merging this makes `main`'s full mutation run gating; the next full run after merge should be clean, since every local run is.
Mutation testing showed several keys no test pressed: q and Esc in the
confirm view, 3 after another choice, k or up, a non-letter or capital,
a letter past the last copy, and a itself, plus the esc, up and down key
names. Each now has a fixture, including that q at the confirm prompt
applies nothing. The quit test waits for the exit with a time limit, so
a broken q fails at once instead of hanging until cargo-mutants' timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The confirm view walked the pending resolutions in hash-map order, so
the same choices came out in 5 different orders in 5 runs. It now walks
the conflict list and shows each group's choice in that order. A fixture
with one change of every kind pins the view; fixtures can now set their
terminal size to show such wide lines whole. The title and warning sit
at fixed rows instead of running y arithmetic that a mutant could not
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rendering walked the list writing each group at row y and stopped at the
bottom edge; a group whose expanded diff did not fit wrote nothing at
all, so expanding a long diff blanked the whole list on a short screen.
Each group's lines are now built first and clipped at the bottom edge,
so an over-tall group shows its head and is cut off above the status
bar. Scrolling follows what was drawn: ensure_scroll_visible looks at
the last group the previous frame showed fully, so it needs no height
argument, and the three App-level scroll tests now draw after every
move, as the event loop does.

Co-Authored-By: GLM 5.3 <noreply@anthropic.com>
App::new pre-selected Do nothing for every group, including the ones
with a suggestion, and the Suggested line renders only when nothing is
chosen - so no suggestion ever reached the screen. Groups with a
suggestion now start unchosen, showing the line; groups without one
keep the Do nothing safe default, and identical files still pre-select
Keep original.

That makes the suggestion logic observable, and four fixtures pin it:
newer EXIF on either side, equal EXIF dates suggesting nothing, an mtime
gap of exactly the threshold still suggesting, and one below it leaving
the safe default. Fixtures set mtimes from one shared base time, so
'exactly one hour apart' is exact and not a race.

Co-Authored-By: GLM 5.3 <noreply@anthropic.com>
Applying resolutions clamped a selection left past the end of the list;
a fixture now pins that the selection lands back on the surviving
group. Suggestions are pinned from every angle the scanner can produce:
newer EXIF on either side, equal EXIF dates, an mtime gap exactly at
the threshold and one below it, JPEG bytes under text names and with no
extension at all, and a BMP staying plain - dimensions are read only
for jpg/png names, because that is all the image crate is built to
parse, which also makes is_image's true-mutant equivalent and excluded
with that reason. The crafted fixture JPEGs declare 8-bit precision now
so the image crate can read their size, and they show the expand arrow
for it. Without a terminal the binary lists conflicts instead; two CLI
tests pin that output and that it touches no files.

Co-Authored-By: GLM 5.3 <noreply@anthropic.com>
Make the full mutation run on main gate
All checks were successful
CI / Lint (pull_request) Successful in 35s
CI / Nix checks and package build (pull_request) Successful in 36s
CI / Run tests (pull_request) Successful in 40s
CI / Mutation testing (pull_request) Successful in 2m34s
CI / Nix checks and package build (push) Successful in 11s
CI / Lint (push) Successful in 16s
CI / Run tests (push) Successful in 21s
CI / Mutation testing (push) Successful in 5m3s
33c780adb2
Every mutant from the last full run is now caught or excluded as
equivalent, so the report-only free pass comes off the all-code job and
it fails on any missed mutant, like the per-PR job already does.

Co-Authored-By: GLM 5.3 <noreply@anthropic.com>
cyplo merged commit 33c780adb2 into main 2026-11-09 10:29:15 +00:00
cyplo deleted branch close-mutation-gaps 2026-11-09 10:29:15 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
cyplo/deescalate!6
No description provided.