test(scripts): kill check_doc_claims.py's 25 surviving mutants for real (#235) - #304
Merged
Conversation
…al (#235) Issue #235's originating mutation run found 25 survivors in check_doc_claims.py's canonical-source helpers. Before this could be fixed, #294 split the file into check_doc_claims.py/doc_claim_sources.py/doc_claim_scan.py/doc_claim_structural.py, moving canonical_reference_count/canonical_tool_counts/canonical_version/ canonical_mechanism_count into doc_claim_sources.py. A fresh scoped mutmut run on current main reports 0 survived — but that number was misleading: check_doc_claims.py reaches doc_claim_sources.py through a bare `import doc_claim_sources`, which gives every function in it `__module__ == "doc_claim_sources"`, never the dotted `"scripts.doc_claim_sources"` mutmut's trampoline expects (the same bare-import attribution defect issue #292 tracks). All 108 of doc_claim_sources.py's mutants were reported "no tests" — an uncalled trampoline, not a verified kill — so #235's specific acceptance criterion ("0 surviving non-equivalent mutants ... in the canonical-source helpers") was unverifiable on main, not satisfied. Fixed by loading doc_claim_sources.py a second time under its own dotted name (the same pattern already used for doc_claim_structural.check_badge_floor) and adding CanonicalSourceDirectTests, which calls through that reference with an explicit read_fn. Re-running the scoped mutation check: all 108 previously invisible mutants are now reachable, and mutmut kills every one of them (260 killed / 125 no-tests / 0 survived across the four files, up from 152/233/0). The remaining 125 "no tests" are pre-existing, already-filed scope (doc_claim_scan.py + doc_claim_structural.py's other functions + main() itself — issue #292), untouched here. Several of the newly-reachable mutants needed an exact-message assertion (missing-sentence, arithmetic-mismatch, missing-pinned-test, missing-section, no-entries, undeclared-mechanism-count, missing-version) where the existing `assertIn`/behavioural tests only checked a substring or the return value — the same gap class #235's table originally named. Verified: - pytest tests_py/scripts/test_check_doc_claims.py: 70 passed, 9 subtests - pytest tests_py/scripts/ (sibling scope): 458 passed, 116 subtests - Full suite: 6835 passed, 5 skipped, 116 subtests passed - ruff check / ruff format --check on the touched file: clean - scripts/check_doc_claims.py --test-count 6840: doc claims OK - scripts/generate_repo_badges.py --check --test-count 6840: badges OK (assets/badge-tests.svg's floor of 6788 <= 6840 holds; no regeneration needed) - Scoped mutmut re-run (tests_py/scripts/test_check_doc_claims.py against scripts/check_doc_claims.py + doc_claim_sources.py + doc_claim_scan.py + doc_claim_structural.py): 260 killed / 125 no-tests (pre-existing #292 scope) / 0 survived, 0 unexplained Closes #235 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust
added a commit
that referenced
this pull request
Jul 30, 2026
…ining functions + repo_badge_catalog for real mutation coverage (#292) check_doc_claims.py bare-imports its siblings doc_claim_scan and doc_claim_structural; generate_repo_badges.py bare-imports repo_badge_catalog. A function's __module__ is fixed at definition time to whatever name it was imported under, and mutmut's trampoline only activates a mutant when that matches the dotted, path-derived name it generated the mutant under -- so every mutant in these bare-imported siblings reported "no tests" under a scoped run, even though the functions are exercised by real passing tests through the bare-imported path (same defect class as badge_render.py/check_badge_floor, issues #262/#280/#293, and doc_claim_sources.py's own instance of it, already fixed on main by #304/issue #235). Fix: dotted-load each sibling under its own "scripts.<name>" spec (mirroring the existing check_badge_floor/doc_claim_sources precedent) and add direct test classes that call through those dotted references instead of through gate's/gen's bare-imported copies. Exact assertEqual on every message string and dict field (not assertIn) to kill the underlying string-literal mutants directly, plus targeted continue-vs- break and split/rsplit/maxsplit tests for the loop- and parsing-shaped mutants exact-match assertions alone don't reach. Verified with a real tally, not the absence of an error: a scoped mutmut run (scripts/mutation_check.sh) against doc_claim_structural.py, doc_claim_scan.py, doc_claim_sources.py, repo_badge_catalog.py and both test files reports "0 surviving mutants" (301 mutants, 301 killed, 0 "no tests", 0 survived). Full suite green (6919 passed, 5 skipped, 121 subtests), doc-claims and badge-floor gates green, ruff clean. Closes #292 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
cdeust
added a commit
that referenced
this pull request
Jul 30, 2026
…ge_catalog for real mutation coverage (#292) (#305) * fix(mutmut): dotted-load doc_claim_scan + doc_claim_structural's remaining functions + repo_badge_catalog for real mutation coverage (#292) check_doc_claims.py bare-imports its siblings doc_claim_scan and doc_claim_structural; generate_repo_badges.py bare-imports repo_badge_catalog. A function's __module__ is fixed at definition time to whatever name it was imported under, and mutmut's trampoline only activates a mutant when that matches the dotted, path-derived name it generated the mutant under -- so every mutant in these bare-imported siblings reported "no tests" under a scoped run, even though the functions are exercised by real passing tests through the bare-imported path (same defect class as badge_render.py/check_badge_floor, issues #262/#280/#293, and doc_claim_sources.py's own instance of it, already fixed on main by #304/issue #235). Fix: dotted-load each sibling under its own "scripts.<name>" spec (mirroring the existing check_badge_floor/doc_claim_sources precedent) and add direct test classes that call through those dotted references instead of through gate's/gen's bare-imported copies. Exact assertEqual on every message string and dict field (not assertIn) to kill the underlying string-literal mutants directly, plus targeted continue-vs- break and split/rsplit/maxsplit tests for the loop- and parsing-shaped mutants exact-match assertions alone don't reach. Verified with a real tally, not the absence of an error: a scoped mutmut run (scripts/mutation_check.sh) against doc_claim_structural.py, doc_claim_scan.py, doc_claim_sources.py, repo_badge_catalog.py and both test files reports "0 surviving mutants" (301 mutants, 301 killed, 0 "no tests", 0 survived). Full suite green (6919 passed, 5 skipped, 121 subtests), doc-claims and badge-floor gates green, ruff clean. Closes #292 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u * docs(changelog): add Unreleased entry for issue #292's mutmut fix Every prior fix in this defect family (badge_render/check_badge_floor #262/#280, doc_claim_sources #235, and #293 itself) has a matching CHANGELOG entry under [Unreleased] ### Fixed; this fix's own commit lacked one. Boy-scout (coding-standards.md §14): the gap was visible the moment the touched diff was compared against the repo's own established convention for this exact class of change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #235.
Issue #235's originating mutation run found 25 survivors in
check_doc_claims.py's canonical-source helpers (canonical_reference_count10,
canonical_tool_counts7,canonical_version3,canonical_mechanism_count3,
read2). Before it could be fixed, #294 merged and split the file intocheck_doc_claims.py/doc_claim_sources.py/doc_claim_scan.py/doc_claim_structural.py— moving all fourcanonical_*helpers intodoc_claim_sources.py.Re-measuring against current main first (per this issue's own instruction):
a fresh scoped
mutmutrun reports 0 survived — but that number ismisleading, not a genuine pass.
check_doc_claims.pyreachesdoc_claim_sources.pythrough a bareimport doc_claim_sources, which givesevery function in that file
__module__ == "doc_claim_sources"— never thedotted
"scripts.doc_claim_sources"mutmut's trampoline expects. This is theexact bare-import attribution defect issue #292 already tracks (and had
flagged
doc_claim_sources.pyas "not yet verified, assumed affected"). All108 of
doc_claim_sources.py's mutants were showing "no tests" — anuncalled trampoline, not a verified kill — so #235's specific acceptance
criterion ("0 surviving non-equivalent mutants ... in the canonical-source
helpers") was unverifiable on main, not actually satisfied. Closing the
issue on the strength of "0 survived" alone would have been trusting a
measurement the tooling could not make.
Fix
tests_py/scripts/test_check_doc_claims.pynow loadsdoc_claim_sources.pyasecond time under its own dotted name (
"scripts.doc_claim_sources") — thesame pattern already established in this file for
doc_claim_structural.check_badge_floor— and a newCanonicalSourceDirectTestsclass calls through that reference with anexplicit
read_fn(these functions already take one; nogate.readmonkeypatch needed).
Re-running the scoped mutation check after adding it: all 108 previously
invisible mutants become reachable, and every one is killed. Final:
260 killed / 125 no-tests / 0 survived across the four files (up from
152/233/0). The remaining 125 "no tests" are pre-existing, already-filed scope
—
doc_claim_scan.py's functions,doc_claim_structural.py's other functions(
check_badge,check_no_hotlinked_badges,check_no_conflict_markers,check_scanned_json_parses), andcheck_doc_claims.py'smain()— all namedin issue #292's "Remaining scope", untouched by this PR (no new issue opened;
#292 already covers it).
Several of the newly-reachable mutants needed an exact-message assertion
where the existing behavioural tests only checked a substring (
assertIn) orjust the return value — missing-sentence, arithmetic-mismatch,
missing-pinned-test, missing-section, no-entries-found,
undeclared-mechanism-count, missing-version-declaration. These are the same
gap class #235's own table named (e.g.
canonical_version()'s "not found" armhad no test at all). No equivalent mutants were needed — every real gap was
killable once the message was pinned.
Acceptance criteria (from #235)
scripts/check_doc_claims.py(+ its extracted siblings
doc_claim_sources.py/doc_claim_scan.py/doc_claim_structural.py, which now carry the logic check_doc_claims.py: 25 surviving mutants in the canonical-source helpers #235's tablenamed) — verified, not just measured: 0 survived, and the 108
previously-unattributed mutants are now demonstrably reachable and
killed, not silently uncounted.
N/A, no equivalent mutants remained after fixing attribution; all real
gaps were killed outright.
canonical_reference_count's entry filter is pinned against abibliography containing headings, separators and blank lines — already
covered by
test_reference_entries_exclude_headings_and_separators(headings
###, separator---, and blank lines between entries allpresent in that fixture), now also exercised directly (mutation-level,
not just behaviourally) via
CanonicalSourceDirectTests.Completion Ledger
doc_claim_sources.pyunder"scripts.doc_claim_sources"CanonicalSourceDirectTestscanonical_tool_counts(): catalogue read + happy path (direct)CanonicalSourceDirectTests.test_tool_counts_come_from_the_cataloguecanonical_tool_counts(): missing header sentence, exact messagetest_missing_standalone_total_sentence_is_an_errorcanonical_tool_counts(): catalogue/pinned-test drifttest_catalogue_drifting_from_the_pinned_registry_test_is_an_errorcanonical_tool_counts(): arithmetic mismatch, exact messagetest_inconsistent_arithmetic_in_the_catalogue_is_an_errorcanonical_tool_counts(): missing pinned test, exact messagetest_missing_pinned_registry_test_is_an_errorcanonical_reference_count(): happy path (direct)test_reference_count_is_the_number_of_entries_not_the_advertised_numbercanonical_reference_count(): headings/separators excluded (direct)test_reference_entries_exclude_headings_and_separatorscanonical_reference_count(): split-first-not-last semanticstest_the_first_references_heading_is_the_split_pointcanonical_reference_count(): missing section, exact messagetest_bibliography_without_a_references_section_is_an_errorcanonical_reference_count(): no entries found, exact messagetest_a_references_section_with_no_entries_is_an_errorcanonical_mechanism_count(): happy path (direct)test_mechanism_count_is_read_from_the_bibliography_headercanonical_mechanism_count(): undeclared count, exact messagetest_undeclared_mechanism_count_is_an_errorcanonical_version(): happy path (direct)test_version_comes_from_pyprojectcanonical_version(): missing declaration, exact messagetest_missing_version_declaration_is_an_errorTest plan
pytest tests_py/scripts/test_check_doc_claims.py— 70 passed, 9 subtestspytest tests_py/scripts/(full sibling scope) — 458 passed, 116 subtestsruff check+ruff format --checkon the touched file — cleanpython scripts/check_doc_claims.py --test-count 6840—doc claims OK (1 declared not-a-claim exemption(s))python scripts/generate_repo_badges.py --check --test-count 6840—badges OK (5 checked)(the tests badge is a monotone floor — 6788 <= 6840 — so no regeneration is needed for a tests-only PR)pyrightconfig.jsonexcludestests_py/scriptsfrom the zero-diagnostic gate — N/A for this change.bestpractices.jsonstill parsesmutmutre-run (tests_py/scripts/test_check_doc_claims.pyagainstscripts/check_doc_claims.py+doc_claim_sources.py+doc_claim_scan.py+doc_claim_structural.py): 260 killed / 125 no-tests (pre-existing mutmut: sibling-imported scripts/ modules show every mutant as 'no tests' (bare-import __module__ mismatch) #292 scope) / 0 survived, 0 unexplainedpyproject.toml's committed[tool.mutmut]scope (mcp_server/shared/json_native.py) untouched — every scoped run restored it viascripts/mutation_check.sh's own trap/cleanupAST size gate (coding-standards.md §4, this repo's 300/40 tightening)
File-level cap: this test file was already at 721 lines before this PR (organized
by one
unittest.TestCaseclass per function/concern — the repo's owntest_generate_pip_constraints.py(944),test_launcher_deps.py(1500), andtest_generate_repo_badges.py(613) are the same, pre-existing pattern forthis test tree). No new method is over the 40-line cap (the AST walk above
reports zero
FN OVER CAPlines).Boy-scout items (coding-standards.md §14)
None seen beyond this issue's own subject in the touched file. The
doc_claim_scan.py/doc_claim_structural.py/main()attribution gaps thisPR's investigation surfaced are pre-existing and already tracked by issue
#292 (filed, with acceptance criteria) — left untouched per §14.3 (outside
this change's blast radius; #235's acceptance criteria name only the
canonical-source helpers, not the whole file's mutation coverage).
🤖 Generated with Claude Code
https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u