Skip to content

fix(shared): kill 9 real json_native mutants, document 5 as equivalent (#250) - #272

Merged
cdeust merged 4 commits into
mainfrom
fix-json-native-mutants-250
Jul 29, 2026
Merged

fix(shared): kill 9 real json_native mutants, document 5 as equivalent (#250)#272
cdeust merged 4 commits into
mainfrom
fix-json-native-mutants-250

Conversation

@cdeust

@cdeust cdeust commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #250. mcp_server/shared/json_native.py is this repo's demonstrated
mutation-testing module ([tool.mutmut]'s committed scope) — 14 of its 43
mutants survived. Reproduced exactly (uv run mutmut run, no arguments):
43/43 killed 29, survived 14, the same 14 IDs the issue lists.

Triage:

  • 9 real gaps (mutants 34-42): the 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.
  • 5 documented equivalents (mutants 11, 17, 21, 22, 23) — verified, not
    asserted:
    • bytes.decode("utf-8", …) vs "UTF-8": codecs.lookup() is
      case-insensitive — codecs.lookup("utf-8") is codecs.lookup("UTF-8")
      is True.
    • typing.cast("SupportsFloat", obj)'s type-hint string: cast's
      source (inspect.getsource(typing.cast)) is literally return val
      the first argument is never read at runtime, for any input.

Fix

  1. Added TestTolistFailureLogging to tests_py/shared/test_json_native.py:
    forces a tolist() that raises, then asserts the exact log record
    (record.msg, both record.args) via caplog — kills all 9 real
    survivors.
  2. Documented the 5 equivalents at their use sites (§12.1) with the
    verification command, not just a claim.
  3. Boy-scout (§14): to_json_native was already 48 lines before this
    change — 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

  • Scoped mutation run (committed [tool.mutmut] scope, uv run mutmut run
    with 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.
  • Local full suite: 6527 passed (this machine's local count differs from
    CI's per repo convention — CI is the source of truth for the number
    quoted at merge time).

Completion Ledger

Path introduced by this diff Asserting test / evidence
_decode_bytes returns decoded str, invalid bytes replaced TestContainers::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) Documented equivalent at use site; verified codecs.lookup("utf-8") is codecs.lookup("UTF-8") — no test possible or needed
_coerce_number returns float for Decimal TestScalars::test_decimal_becomes_float (pre-existing, unchanged)
_coerce_number returns None on TypeError/ValueError (complex etc.) → caller falls through Covered transitively by TestFallback::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) Documented equivalent at use site; verified via inspect.getsource(typing.cast)
_tolist_fallback success path recurses into to_json_native TestContainers::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_fallback failure path: logs exact format string + (type(obj), exc), returns sentinel → caller stringifies New: TestTolistFailureLogging::test_logs_type_and_exception_then_falls_back_to_str
to_json_native dispatcher: every branch unchanged in order/semantics Full existing 14-test file green, unchanged assertions

No 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

cdeust and others added 4 commits July 29, 2026 23:32
#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
@cdeust
cdeust merged commit 7f3ebf9 into main Jul 29, 2026
19 checks passed
@cdeust
cdeust deleted the fix-json-native-mutants-250 branch July 29, 2026 22:32
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.

json_native.py: 14 surviving mutants under the repository's own committed mutmut scope

1 participant