Skip to content

test: 241 unpinned mutants in the AST extractor walkers (down from 368) — language extractors have almost no behavioural tests #369

Description

@cdeust

Context

Surfaced while fixing the unbounded AST recursion in the extractor layer (branch fix/ast-walk-type-recursion-depth). A scoped mutation run over the five extractor modules shows the language walkers are largely unpinned by tests.

Measured 2026-08-06, same tool selection both sides:

scripts/mutation_check.sh \
  "tests_py/core/test_ast_extractors.py tests_py/core/test_ast_parser_extra_langs.py tests_py/core/test_ast_extractor_depth.py" \
  mcp_server/core/ast_extractors.py mcp_server/core/ast_extractors_jvm.py \
  mcp_server/core/ast_extractors_clike.py mcp_server/core/ast_extractors_scripting.py \
  mcp_server/core/ast_extractors_extra.py
mutants killed no tests survived
HEAD (before the recursion fix) 1554 475 851 228
after the recursion fix 1486 571 492 423

The survived column rising while no tests falls is reclassification, not regression: the new depth tests execute code that no test reached before, so mutants moved from "never run" into "run but not detected". The honest metric is not-killed = survived + no tests, and it improved in every walker:

function HEAD not-killed after
_walk_for_calls 40 13
_walk_java 59 37
_walk_kotlin 77 54
_walk_csharp 59 36
_walk_ruby 53 33
_walk_php 61 51
_extract_swift_node 19 17
_walk_type 0 0
total 368 241

The gap

241 mutants remain unpinned in those seven walkers, and 423 across the five modules in total. Concretely, the Java, Kotlin, C#, Ruby, PHP and Swift definition extractors have almost no behavioural tests: tests_py/core/test_ast_extractors.py covers Python, JS and Go only, and test_ast_parser_extra_langs.py is thin.

This is pre-existing debt, not introduced by the recursion fix, which is why it was not treated as a blocker on that PR (rules/coding-standards.md §12.5: the per-commit tier does not punish a change for a pre-existing gap). It is filed here rather than left as prose, per §14.3.

Notable non-walker survivors in the same run, same cause: _extract_rust_node 51, extract_go_definitions 34, _extract_js_node 20, _extract_python_class 16, _extract_python_func 15, _collect_call_basenames 14.

Why it matters beyond the score

These extractors decide what enters the code graph. A silent behaviour change in one of them (a wrong qualified name, a dropped nested class, a method attributed to the wrong scope) degrades every downstream consumer — ingest_codebase, wiki reference pages, unified_search — with no failing test and no visible signal. The recursion fix had to lean on a purpose-built differential harness against the old implementation over 2229 sources precisely because the test suite could not have caught an ordering regression.

Acceptance criteria

  1. Behavioural tests for the six language extractors covering, per language: top-level function, class/type with methods, nested type, unnamed/anonymous construct, and the qualified-name shape (Type.method).
  2. Not-killed count for the seven walkers is 0, or each remaining mutant is documented as equivalent with a written rationale at the use site (§12.1).
  3. The same scoped mutation_check.sh invocation above is the verification command, and its output is quoted in the PR.
  4. Consider promoting these modules into memory/critical-zones.conf so the sweep tracks them (§12.5).

Note on the tooling

scripts/mutation_check.sh reported none — 0 surviving mutants 🎉 on this run while its own progress counter ended at 🙁 423. The survivor list from mutmut results is 915 lines and non-empty. The script's grep-based survivor detection therefore produced a false green here; the cleanup trap deletes mutants/ and .mutmut-cache on exit, so the discrepancy cannot be investigated after the fact. Worth a separate look — a mutation gate that can report a false clean is worse than no gate.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions