Skip to content

strip_src_prefix config: support flat layouts where the import package is literally src - #593

Closed
axisrow wants to merge 1 commit into
boxed:mainfrom
axisrow:fix/strip-src-prefix-knob
Closed

axisrow wants to merge 1 commit into
boxed:mainfrom
axisrow:fix/strip-src-prefix-knob

Conversation

@axisrow

@axisrow axisrow commented Oct 7, 2026

Copy link
Copy Markdown

Problem

Projects whose import package is literally src (flat layout: src/__init__.py, imports like from src.calc import ...) cannot run mutmut 3.8 at all — stats collection dies during the first trampoline hit:

AssertionError: Failed trampoline hit. Module name starts with `src.`, which is invalid

The root cause is that mutant naming assumes the src-layout convention, where src/ is only a container directory:

  1. get_mutant_name (utils/format_utils.py) unconditionally strips a src. prefix. For a flat layout the stripped name (collection_queue.x_foo__mutmut_1) no longer matches the imported module (src.collection_queue).
  2. That breaks the trampoline routing in mutation/trampoline.py (module != decorated_func.__module__ never matches → mutants silently never apply), the stats mapping (tests_by_mangled_function_name lookups miss), and finally record_trampoline_hit (mutation/stats.py) asserts on the src.-prefixed recorded name — killing the whole stats run.

Sandbox importability itself already works for flat layouts (setup_source_paths puts mutants/ on sys.path, so from src... resolves to the mutated copy) — only the name shaping is broken.

Fix

New config [tool.mutmut] strip_src_prefix (default true = current behavior, so src-layout projects are unaffected):

  • false: get_mutant_name keeps the full src.-prefixed module path — routing, stats keys, and the assert all become consistent;
  • record_trampoline_hit asserts only while stripping is active, and its message now points flat-layout users at the knob;
  • unit tests for the config key and both get_mutant_name modes;
  • new e2e fixture e2e_projects/flat_src (src as the import package) with a result snapshot: 11 mutants generated with src.-prefixed names, 6 killed / 5 survived — i.e. mutants verifiably apply and route, while boundary-condition mutants (clamp at value == low) survive exactly as the fixture's tests intend.

Verification

  • New e2e snapshot test passes; full local suite: 456 passed. The 3 pre-existing failures on my machine (test_e2e_type_checking ×2 — type-checker version drift, test_safe_setproctitle — macOS CoreFoundation fork) reproduce on a clean checkout without this patch.
  • Verified end-to-end on a real flat-layout project (~1.8k-file Telegram engine, from src.* imports throughout): with the knob, stats collection completes for the first time and mutants execute; stock 3.8 asserts out at the same point.

🤖 Generated with Claude Code

…e is `src`

Projects whose import package is literally `src` (flat layout: `src/__init__.py`,
imports like `from src.calc import ...`) cannot run mutmut 3.8:

1. `get_mutant_name` unconditionally strips a `src.` prefix from module names —
   correct for the src-layout convention (`src/` is only a container), but for a
   flat layout the stripped names no longer match the imported modules, so the
   trampoline routing (`module != decorated_func.__module__`) never applies any
   mutant, the stats keys diverge (`"collection_queue.x_foo"` recorded as
   `"src.collection_queue.x_foo"`), and `record_trampoline_hit` asserts out
   during stats collection.

Add a `[tool.mutmut] strip_src_prefix` config (default `true` = current
behavior). With `false`, mutant names keep the full `src.`-prefixed module
path, which makes routing, stats keys, and the assert all consistent. The
sandbox already imports flat layouts correctly (`mutants/` on sys.path), so no
other changes are needed.

- `configuration.py`: new `strip_src_prefix: bool = True` config field.
- `utils/format_utils.py`: `get_mutant_name` strips only when configured.
- `mutation/stats.py`: the `src.` assert only fires while stripping is active
  (and now points flat-layout users at the new knob).
- New e2e fixture `e2e_projects/flat_src` (`src` as the import package) with a
  result snapshot: 11 mutants generated with `src.`-prefixed names, 6 killed /
  5 survived — mutants verifiably apply and route, boundary-condition mutants
  survive as the tests intend.
- Unit tests for the config key and both `get_mutant_name` modes.

Verified on a real flat-layout project (a ~1.8k-file Telegram engine with
`from src.*` imports throughout): stats collection passes for the first time
and mutants execute.

Co-Authored-By: Claude Code <noreply@anthropic.com>
axisrow added a commit to axisrow/tg_content_factory that referenced this pull request Oct 7, 2026
) unblocks mutation runs

With the upstream fix (strip_src_prefix knob, our branch fix/strip-src-prefix-knob
installed editable) the flat-`src` incompatibility is resolved: enable it and
update the pyproject comment + CLAUDE.md note from "blocked upstream" to the
patched-locally state. Also records the full also_copy set (adk/ joins at run
time — tests/test_adk_app.py loads adk/tg_content_factory/agent.py by path).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow axisrow changed the title strip_src_prefix config: support flat layouts where the import package is strip_src_prefix config: support flat layouts where the import package is literally src Oct 7, 2026
@boxed

boxed commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Projects that do this are broken. Please fix the project. I've closed these issues before, please search closed issues before submitting.

@boxed boxed closed this Oct 7, 2026
axisrow added a commit to axisrow/tg_content_factory that referenced this pull request Oct 8, 2026
) unblocks mutation runs

With the upstream fix (strip_src_prefix knob, our branch fix/strip-src-prefix-knob
installed editable) the flat-`src` incompatibility is resolved: enable it and
update the pyproject comment + CLAUDE.md note from "blocked upstream" to the
patched-locally state. Also records the full also_copy set (adk/ joins at run
time — tests/test_adk_app.py loads adk/tg_content_factory/agent.py by path).

Co-Authored-By: Claude Code <noreply@anthropic.com>
axisrow added a commit to axisrow/tg_content_factory that referenced this pull request Oct 8, 2026
…rc incompatibility (#1501)

* fix(tooling): mutmut sandbox copies root conftest.py + scripts; document the flat-src incompatibility

Root-caused the standing "mutmut is incompatible with this repo's harness"
(BadTestExecutionCommandsException) down to two stacked layers:

1. Sandbox layer (fixed here): mutmut's mutants/ copy included pyproject/src/
   tests but NOT the repo-root conftest.py (tests read it by path —
   test_conftest_xdist.py) nor scripts/ (test_detect_reinvented.py,
   test_doc_coverage.py load tools from it by path) — stats collection died
   with FileNotFoundError before mutmut raised its generic exception.
   [tool.mutmut] now pins source_paths and also_copy for both.

2. Upstream layer (documented, not fixable in-repo): mutmut 3.8 assumes
   src-layout — it unconditionally strips a `src.` prefix from module names
   (mutmut/utils/format_utils.py get_mutant_name) and asserts
   `not name.startswith("src.")` on every trampoline hit
   (mutmut/stats.py record_trampoline_hit). Our import package IS `src`
   (flat layout), so every stats run asserts out. Full mutation runs need an
   upstream fix or a package rename; the per-fix mutation proof stays the
   documented red→green run.

Red→green for layer 1: sandbox collection before the copy list —
FileNotFoundError: mutants/conftest.py, then mutants/scripts/detect_reinvented.py
(two pytest -q --collect-only runs inside mutants/); after — clean collection,
exit 0. mutmut then fails at the documented upstream assert instead.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(tooling): mutmut sandbox ships run-time root reads (.github, docs, adk); gitignore mutants/

Review follow-up to the sandbox-copy fix: the original green proof covered
only --collect-only, while the stats run executes the full suite and tests
read repo-root paths at RUN time. Verified by actually running the suite
inside a fresh mutants/ sandbox:

- before: FileNotFoundError at tests/test_ci_workflow_structure.py
  (.github/workflows/ci.yml), tests/test_agent_tool_smoke_contract.py +
  tests/test_cli_agent_parity.py (docs/reference/*.md),
  tests/test_adk_app.py (adk/tg_content_factory/agent.py)
- after (also_copy += ".github", "docs", "adk" — directory entries; nested
  file paths crash mutmut's copier): 1714 passed, 0 missing-file failures,
  141 skipped. Remaining diff vs the real repo: one GC-timing
  PytestUnraisableExceptionWarning (unclosed sqlite3.Connection) in
  tests/test_adk_backend.py::test_chat_stream_skips_final_aggregated_text —
  passes in the real repo (0.18s), not a missing-copy problem.

Also: mutants/ is gitignored (same class as .serena/), CLAUDE.md's
`mutmut run` entry carries the upstream-blocker caveat, and the pyproject
comment states the data/-dir exclusion (live DB) and the rm -rf wipe
(mutmut never prunes the sandbox).

Refs the flat-src upstream incompatibility documented in the previous
commit — full mutation runs still require an upstream fix or a package
rename.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(tooling): enable strip_src_prefix — patched mutmut (boxed/mutmut#593) unblocks mutation runs

With the upstream fix (strip_src_prefix knob, our branch fix/strip-src-prefix-knob
installed editable) the flat-`src` incompatibility is resolved: enable it and
update the pyproject comment + CLAUDE.md note from "blocked upstream" to the
patched-locally state. Also records the full also_copy set (adk/ joins at run
time — tests/test_adk_app.py loads adk/tg_content_factory/agent.py by path).

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(tooling): mutmut opt-out via mutmut_skip marker; sandbox config finalized

Stats-baseline convergence over 6 sandbox runs (8273 green tests at the last
failure point): the tests that cannot run under mutmut are now marked, not
deselected by stale-prone node-id lists:

- new `mutmut_skip` marker (same idiom as `slow`); [tool.mutmut]
  pytest_add_cli_args shrinks to `-p no:unraisableexception -m 'not
  mutmut_skip'`;
- tagged: 4 repo-contract AST/surface scans (cli_pool_classification,
  client_pool_decomposition, client_pool_lease_stress, db_access_conventions
  x2) — in the sandbox they see trampoline-generated copies, and for mutation
  semantics a scan that fails on any change would false-kill every mutant it
  touches; test_mcp_server_stdio (both tests spawn the MCP subprocess, which
  dies on closed stdio fds under the forkserver runner); quality_scoring
  (known network-flaky family, poison for a stats baseline);
- also_copy += mkdocs.yml, config.yaml, docker-compose.yml (tests read them
  from the repo root at run time);
- unraisable-plugin disable stays run-wide: the serial stats run GC-finalizes
  leaky leftovers from earlier tests and the plugin attributes them to random
  tests (observed at 1713 and 3673 green tests).

The normal suite keeps full strictness — every opt-out lives only in the
mutmut invocation.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(tooling): review fixes for mutmut config — per-test mutmut_skip, threadexception off, sandbox narrowing

---------

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
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.

2 participants