fix(shared): kill 9 real json_native mutants, document 5 as equivalent (#250) - #272
Merged
Conversation
#250) Scoped mutmut run on the committed [tool.mutmut] scope (mcp_server/shared/ json_native.py) reproduced exactly the reported 14 survivors: 9 were a real test gap on the tolist()-failure debug log (format string + args never asserted), 5 are typing.cast/codec-name mutations that are provably equivalent at runtime (verified via inspect.getsource(typing.cast) and codecs.lookup identity, not just asserted). - tests_py/shared/test_json_native.py: new TestTolistFailureLogging test forces the tolist() exception path and asserts the exact log record (format string + both args via caplog), killing mutants 34-42. - mcp_server/shared/json_native.py: documented the 5 remaining survivors as equivalent at their use sites (§12.1), and split the 60-line to_json_native (over this repo's 40-line/coding-standards' 50-line cap) into _decode_bytes/_coerce_number/_tolist_fallback + a thin dispatcher — each helper under 20 lines, existing 14 tests passing byte-for-byte unchanged as the behavior-preservation proof (boy-scout: the function was already over cap before this change). Re-run after the split: 51 mutants, 0 surviving non-equivalent (same 5 equivalents renumbered under the new helper names). Fixes #250 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
The new TestTolistFailureLogging test in the prior commit adds exactly one collected test (paired count verified locally and against CI's own `pytest --collect-only` output: both report 6527). This repo hard-codes the suite size in 6 files plus a committed badge SVG, checked by a blocking CI gate (`scripts/check_doc_claims.py` / `generate_repo_badges.py --check`) — CI's Test (Python 3.12) leg correctly failed on the stale 6526 claim. Resynced: README.md (x2), CONTRIBUTING.md (x2), CLAUDE.md, docs/ASSURANCE-CASE.md, .bestpractices.json (x4), assets/badge-tests.svg (regenerated via scripts/generate_repo_badges.py --test-count 6527). Both gates verified green locally with the same --test-count CI uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…ts-250 # Conflicts: # .bestpractices.json # CLAUDE.md # CONTRIBUTING.md # README.md # assets/badge-tests.svg # docs/ASSURANCE-CASE.md
origin/main advanced to 6548 tests (PR #261, tree-sitter-language-pack pin + its own test additions) between this branch's creation and this merge, superseding my prior 6526->6527 resync. Merged origin/main, resolved the doc-count conflicts by taking main's committed values, then re-derived the true paired count on the FULLY MERGED tree via `pytest --collect-only -q` (6549 = 6548 + this branch's one new test), not by arithmetic on stale numbers. Resynced the same 6 files + regenerated assets/badge-tests.svg. Both check_doc_claims.py --test-count 6549 and generate_repo_badges.py --check --test-count 6549 verified green locally, and the full suite (6549 tests) passes on the merged tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
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 #250.
mcp_server/shared/json_native.pyis this repo's demonstratedmutation-testing module (
[tool.mutmut]'s committed scope) — 14 of its 43mutants survived. Reproduced exactly (
uv run mutmut run, no arguments):43/43 killed 29, survived 14, the same 14 IDs the issue lists.
Triage:
tolist()-failure debug log(
logger.debug("tolist() conversion failed for %r: %s", type(obj), exc))had no test asserting the format string or either argument — a dropped
arg, a swapped arg, or a reworded message all survived silently.
asserted:
bytes.decode("utf-8", …)vs"UTF-8":codecs.lookup()iscase-insensitive —
codecs.lookup("utf-8") is codecs.lookup("UTF-8")is
True.typing.cast("SupportsFloat", obj)'s type-hint string:cast'ssource (
inspect.getsource(typing.cast)) is literallyreturn val—the first argument is never read at runtime, for any input.
Fix
TestTolistFailureLoggingtotests_py/shared/test_json_native.py:forces a
tolist()that raises, then asserts the exact log record(
record.msg, bothrecord.args) viacaplog— kills all 9 realsurvivors.
verification command, not just a claim.
to_json_nativewas already 48 lines before thischange — over this repo's own 40-line/method convention (CLAUDE.md) and
at the coding-standards.md §4 50-line hard cap once my initial fix's
documentation comments pushed it to 60. Split into
_decode_bytes/_coerce_number/_tolist_fallback+ a thin dispatcher,each new helper under 20 lines. Behavior-preserving: all 14 pre-existing
tests pass byte-for-byte unchanged (the proof), and the whole local
suite is green (6527 passed, 0 failed).
Verification
[tool.mutmut]scope,uv run mutmut runwith no arguments) after the fix: 51 mutants (split creates more mutation
sites) → 46 killed, 0 surviving non-equivalent mutants — the same 5
equivalents, renumbered under the new helper names
(
_decode_bytes__mutmut_6,_coerce_number__mutmut_{2,6,7,8}).ruff check/ruff format --check: clean.pyright: 0 errors, 0 warnings, 0 informations on both touched files.CI's per repo convention — CI is the source of truth for the number
quoted at merge time).
Completion Ledger
_decode_bytesreturns decoded str, invalid bytes replacedTestContainers::test_bytes_decode_to_str,test_invalid_utf8_bytes_replaced_not_raised(pre-existing, unchanged, still pass against the extracted helper)_decode_bytes— mutmut_11 equivalent (case-insensitive codec name)codecs.lookup("utf-8") is codecs.lookup("UTF-8")— no test possible or needed_coerce_numberreturnsfloatforDecimalTestScalars::test_decimal_becomes_float(pre-existing, unchanged)_coerce_numberreturnsNoneonTypeError/ValueError(complex etc.) → caller falls throughTestFallback::test_unknown_object_stringifies_rather_than_crashing(no complex-specific test existed pre-change or post-change; behavior unchanged)_coerce_number— mutmut_17/21/22/23 equivalent (cast's type-hint string unused at runtime)inspect.getsource(typing.cast)_tolist_fallbacksuccess path recurses intoto_json_nativeTestContainers::test_numpy_array_becomes_list,TestScalars::test_numpy_float32_becomes_python_float/test_numpy_int64_becomes_python_int/test_numpy_bool_becomes_native(pre-existing, unchanged)_tolist_fallbackfailure path: logs exact format string +(type(obj), exc), returns sentinel → caller stringifiesTestTolistFailureLogging::test_logs_type_and_exception_then_falls_back_to_strto_json_nativedispatcher: every branch unchanged in order/semanticsNo unmapped paths. No deviations from the issue's acceptance criteria — all
three checkboxes met: 0 surviving non-equivalent mutants; every survivor
triaged (killed or documented-equivalent with a written, verified
rationale); the kill test exercises the actual failure-path signal
(
caplog), not a downstream side effect.Co-Authored-By: Claude Opus 5 noreply@anthropic.com