Skip to content

Harden the mutmut runner against env leaks and test-only scopes (#369) - #374

Open
leynos wants to merge 4 commits into
mainfrom
fix/mutmut-baseline-hygiene
Open

Harden the mutmut runner against env leaks and test-only scopes (#369)#374
leynos wants to merge 4 commits into
mainfrom
fix/mutmut-baseline-hygiene

Conversation

@leynos

@leynos leynos commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two mutmut-side baseline-hygiene defects broke scheduled mutation runs:

  • shared-actions#369 — the reusable workflow sets INPUT_* variables on
    the "Run mutation testing" step. mutmut runs the caller's test suite
    in-process as its baseline, inheriting that environment, so
    INPUT_MODULE_PREFIX_STRIP="" (set by this repo's own
    mutation-testing-caller.yml, whose sources live at the repo root rather
    than under src/) leaked into the baseline. The module-glob test read the
    ambient value and observed "" instead of the documented "src/" default,
    failing every scheduled run since 2026-07-10.
  • agent-helper-scripts#74 — a change-detection window containing only
    test-file edits mapped a test path straight onto a mutmut module filter that
    matches no mutants, so mutmut run aborted with
    AssertionError: Filtered for specific mutants, but nothing matches.

Changes

Both fixes live in workflow_scripts/mutation_run_mutmut.py:

  • Strip INPUT_* before mutmut run. _run_mutation_testing now runs the
    subprocess inside a local.env() block that deletes every INPUT_*
    variable, so workflow inputs configure the script but never reach the mutated
    project's in-process pytest baseline. The block restores the environment on
    exit, leaving the later mutmut results call unaffected.
  • Isolate the affected tests from ambient env. _prepare clears the
    INPUT_* variables so tests exercise the script's documented defaults rather
    than whatever the surrounding CI environment happens to set.
  • Treat test modules as non-mutable source. _is_test_file skips
    conftest.py, test_*, *_test, and files under a test/tests
    directory. A scope of test files alone now yields no globs and takes the
    existing short-circuit path.
  • Graceful no-op with a clear summary note. The short-circuit records
    changed files contain no mutable source to GITHUB_STEP_SUMMARY instead of
    silently returning, matching the has_changes=false skip semantics.

Review walkthrough

In
workflow_scripts/mutation_run_mutmut.py:

  • _is_test_file / _module_glob_for — test-file exclusion.
  • _workflow_input_env_names / _run_mutation_testing — env sanitisation.
  • _write_no_source_summary / main — graceful no-op summary.

In
workflow_scripts/tests/test_mutation_run_mutmut.py:

  • fake_uv now dumps the subprocess environment; new tests cover the env
    leak, the test-only short-circuit, and test-file exclusion (red-green: all
    failed before the fix).

Validation

make check-fmt, make lint, make typecheck, make test, make spelling
run locally on the branch.

Closes #369
Fixes leynos/agent-helper-scripts#74

References

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

The mutation workflow excludes pytest modules from mutmut filters, removes workflow INPUT_* variables before mutation runs, and short-circuits test-only scopes with an empty results file and no-op summary.

Mutation scope safeguards

Layer / File(s) Summary
Changed-file mutation filtering
workflow_scripts/mutation_run_mutmut.py, workflow_scripts/tests/test_mutation_run_mutmut.py, docs/mutation-mutmut-workflow.md
Test modules are excluded from mutant globs, including conftest.py, test stems, and test directories; documentation and tests describe the behaviour.
Mutmut environment isolation
workflow_scripts/mutation_run_mutmut.py, workflow_scripts/tests/test_mutation_run_mutmut.py
The mutmut subprocess runs without workflow INPUT_* variables, with tests capturing and checking its environment.
No-source short-circuit
workflow_scripts/mutation_run_mutmut.py, workflow_scripts/tests/test_mutation_run_mutmut.py
Test-only changes avoid invoking mutmut, write empty results, and append a dedicated no-source summary.

Sequence Diagram(s)

sequenceDiagram
  participant ChangedFiles
  participant _module_glob_for
  participant main
  participant GITHUB_STEP_SUMMARY
  ChangedFiles->>_module_glob_for: provide changed Python paths
  _module_glob_for->>main: return no mutable globs
  main->>GITHUB_STEP_SUMMARY: append no-source summary
Loading

Suggested labels: Issue

Suggested reviewers: codescene-access

Poem

Test paths drift past the mutant gate,
Input vars no longer infiltrate.
Empty results mark the quiet source,
Summaries chart the gentler course.
Mutation runs now know their fate.

🚥 Pre-merge checks | ✅ 18 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
User-Facing Documentation ⚠️ Warning No docs/users-guide.md exists; the new mutmut behaviour is only documented in docs/mutation-mutmut-workflow.md and the README signpost. Create or update docs/users-guide.md with the changed mutmut scoping, test-only no-op, and env-sanitisation behaviour, then keep the README link aligned.
Testing (Property / Proof) ⚠️ Warning The patch adds broad path-classification and INPUT_* filtering invariants, but only example tests were added; the repo already uses Hypothesis for similar helper invariants. Add Hypothesis properties for test-file exclusion and INPUT_* sanitisation, or document why example tests fully cover the invariant.
✅ Passed checks (18 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the #369 and #74 fixes: env sanitisation, test-file exclusion, and a no-op summary.
Out of Scope Changes check ✅ Passed The changes stay within mutation-runner code, tests, and docs, with no unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Testing (Overall) ✅ Passed PASS: the new tests exercise real behaviour end-to-end: test-file exclusion, no-source short-circuit, and INPUT_* sanitisation across the uv subprocess boundary.
Developer Documentation ✅ Passed PASS: The mutmut helper boundary is documented in developers-guide.md, the workflow design doc was updated for test-file exclusion/no-source short-circuit, and the execplan stays current.
Module-Level Documentation ✅ Passed Accept the check: both touched Python modules carry clear top-level docstrings that state purpose and workflow relationship.
Testing (Unit And Behavioural) ✅ Passed New unit tests cover file-to-glob edge cases; app-level tests drive the CLI boundary with fake uv, env sanitisation, and the no-source short-circuit.
Testing (Compile-Time / Ui) ✅ Passed Python runtime changes only; no trybuild analogue applies, and the PR adds focused unit tests for glob translation and summary/no-op output.
Unit Architecture ✅ Passed PASS: The patch only swaps explicit stem checks for a compiled regex in the pure _is_test_file query; it adds no new side effects or hidden dependencies.
Domain Architecture ✅ Passed PASS: The patch stays in workflow_scripts/docs/tests; it handles mutmut input translation and env sanitisation at the workflow boundary, leaving domain code untouched.
Observability ✅ Passed The script logs the mutmut command, exit code, result counts, and the new no-source outcome, so the new CI states are diagnosable.
Security And Privacy ✅ Passed No secrets, auth gaps, broad permissions, or data exposure appear; the change only strips INPUT_* from the subprocess env and adds a non-sensitive no-source summary.
Performance And Resource Use ✅ Passed All new work is linear and bounded: one pass over file names/env vars, a compiled regex, and a test-only short-circuit that skips mutmut and extra I/O.
Concurrency And State ✅ Passed PASS: Environment mutation is scoped to a local.env context that restores on exit; no async/shared-worker state was added, and tests cover env leakage plus the no-op path.
Architectural Complexity And Maintainability ✅ Passed PASS: Changes stay local to one script; new helpers only encode immediate test-file/env predicates and a summary snippet, with no new layers, dependencies, or hidden orchestration.
Rust Compiler Lint Integrity ✅ Passed Only workflow_scripts/mutation_run_mutmut.py changed; no Rust files, lint suppressions, or ownership/cloning changes were introduced.
Title check ✅ Passed The title matches the PR’s main fixes for environment leaks and test-only scopes, and includes the linked issue.
Description check ✅ Passed The description is directly about the mutmut runner hardening, test-file exclusion, and no-op summary behaviour.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mutmut-baseline-hygiene

Comment @coderabbitai help to get the list of available commands.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos pushed a commit that referenced this pull request Jul 21, 2026
Replace the three-clause stem conditional in _is_test_file with a
compiled regex and a named directory-name set, resolving the CodeScene
Complex Conditional finding on PR #374.
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review July 26, 2026 22:46

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8a4acf36f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +128 to +130
if TEST_STEM_RE.match(path.stem):
return True
return any(part in TEST_DIR_NAMES for part in path.parts[:-1])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect caller-configured mutable source paths

When a caller intentionally includes a production module whose path matches this heuristic—for example, source_paths = ["src/"] with src/pkg/tests/helpers.py or src/pkg/request_test.py—the predicate drops that changed file even though mutmut is configured to generate mutants for it. A scheduled run will then omit the module or take the successful “no mutable source” path, silently losing mutation coverage. Test exclusions should be derived from the caller's mutmut configuration or known test-selection paths rather than inferred solely from directory and filename conventions.

Useful? React with 👍 / 👎.

codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the Issue label Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
workflow_scripts/mutation_run_mutmut.py (1)

119-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim private helper docstrings to a single-line summary.

_is_test_file, _module_glob_for, _workflow_input_env_names, and _write_no_source_summary are all private (underscore-prefixed) yet carry multi-paragraph docstrings. Move the rationale into inline comments or fold it into the module-level notes instead.

As per path instructions: "Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces."

✏️ Example trim for `_is_test_file`
 def _is_test_file(path: PurePosixPath) -> bool:
-    """Return True when ``path`` is a pytest test module, not source.
-
-    mutmut only generates mutants for source files, so a change window of
-    test modules alone maps to no mutants and would abort ``mutmut run``
-    with an empty filter. Files whose stem matches ``TEST_STEM_RE`` or
-    that sit under a ``test``/``tests`` directory are treated as
-    non-mutable and skipped.
-    """
+    """Return True when ``path`` is a pytest test module, not source."""

Apply the same trim to _module_glob_for, _workflow_input_env_names, and _write_no_source_summary.

Also applies to: 281-290, 332-341

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow_scripts/mutation_run_mutmut.py` around lines 119 - 152, Trim the
multi-line docstrings of private helpers _is_test_file, _module_glob_for,
_workflow_input_env_names, and _write_no_source_summary to single-line
summaries. Preserve any rationale needed for maintainers by moving it to inline
comments or module-level notes, without changing the helpers’ behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workflow_scripts/tests/test_mutation_run_mutmut.py`:
- Around line 226-247: Update test_workflow_input_env_does_not_leak_into_mutmut
to remove the unused fake_uv parameter and add
pytest.mark.usefixtures("fake_uv") so the fixture's PATH setup still occurs
without triggering Ruff's unused-argument check.

---

Outside diff comments:
In `@workflow_scripts/mutation_run_mutmut.py`:
- Around line 119-152: Trim the multi-line docstrings of private helpers
_is_test_file, _module_glob_for, _workflow_input_env_names, and
_write_no_source_summary to single-line summaries. Preserve any rationale needed
for maintainers by moving it to inline comments or module-level notes, without
changing the helpers’ behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5219497-0e2d-4971-8121-fbf401fd534c

📥 Commits

Reviewing files that changed from the base of the PR and between 074f7d8 and c8a4acf.

📒 Files selected for processing (3)
  • docs/mutation-mutmut-workflow.md
  • workflow_scripts/mutation_run_mutmut.py
  • workflow_scripts/tests/test_mutation_run_mutmut.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/polythene (auto-detected)

Comment on lines +226 to +247
@POSIX_SHIMS_ONLY
def test_workflow_input_env_does_not_leak_into_mutmut(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fake_uv: Path
) -> None:
"""``INPUT_*`` vars are stripped before mutmut runs the baseline.

Reproduces shared-actions#369: the caller sets
``INPUT_MODULE_PREFIX_STRIP=""``; without sanitisation that value
reaches mutmut's in-process pytest baseline and breaks tests that
read the ambient environment.
"""
self._prepare(tmp_path, monkeypatch)
monkeypatch.setenv("INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setitem(local.env, "INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setenv("INPUT_MODULE_PREFIX_STRIP", "")
monkeypatch.setitem(local.env, "INPUT_MODULE_PREFIX_STRIP", "")
run_mutmut.app([])
env_dump = (tmp_path / "uv-env.txt").read_text(encoding="utf-8")
leaked = [line for line in env_dump.splitlines() if line.startswith("INPUT_")]
assert not leaked, (
f"no INPUT_* variable should reach the mutmut subprocess, found {leaked}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Drop the unused fake_uv parameter; use usefixtures instead.

fake_uv is only needed for its side effect of installing the shim on PATH; the test body never touches it, hence Ruff's ARG002. Renaming the parameter would break pytest's name-based fixture resolution, so switch to @pytest.mark.usefixtures("fake_uv").

🧹 Proposed fix
+    `@pytest.mark.usefixtures`("fake_uv")
     `@POSIX_SHIMS_ONLY`
     def test_workflow_input_env_does_not_leak_into_mutmut(
-        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fake_uv: Path
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
     ) -> None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@POSIX_SHIMS_ONLY
def test_workflow_input_env_does_not_leak_into_mutmut(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, fake_uv: Path
) -> None:
"""``INPUT_*`` vars are stripped before mutmut runs the baseline.
Reproduces shared-actions#369: the caller sets
``INPUT_MODULE_PREFIX_STRIP=""``; without sanitisation that value
reaches mutmut's in-process pytest baseline and breaks tests that
read the ambient environment.
"""
self._prepare(tmp_path, monkeypatch)
monkeypatch.setenv("INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setitem(local.env, "INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setenv("INPUT_MODULE_PREFIX_STRIP", "")
monkeypatch.setitem(local.env, "INPUT_MODULE_PREFIX_STRIP", "")
run_mutmut.app([])
env_dump = (tmp_path / "uv-env.txt").read_text(encoding="utf-8")
leaked = [line for line in env_dump.splitlines() if line.startswith("INPUT_")]
assert not leaked, (
f"no INPUT_* variable should reach the mutmut subprocess, found {leaked}"
)
`@pytest.mark.usefixtures`("fake_uv")
`@POSIX_SHIMS_ONLY`
def test_workflow_input_env_does_not_leak_into_mutmut(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""``INPUT_*`` vars are stripped before mutmut runs the baseline.
Reproduces shared-actions#369: the caller sets
``INPUT_MODULE_PREFIX_STRIP=""``; without sanitisation that value
reaches mutmut's in-process pytest baseline and breaks tests that
read the ambient environment.
"""
self._prepare(tmp_path, monkeypatch)
monkeypatch.setenv("INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setitem(local.env, "INPUT_FILES", "src/mypkg/calc.py")
monkeypatch.setenv("INPUT_MODULE_PREFIX_STRIP", "")
monkeypatch.setitem(local.env, "INPUT_MODULE_PREFIX_STRIP", "")
run_mutmut.app([])
env_dump = (tmp_path / "uv-env.txt").read_text(encoding="utf-8")
leaked = [line for line in env_dump.splitlines() if line.startswith("INPUT_")]
assert not leaked, (
f"no INPUT_* variable should reach the mutmut subprocess, found {leaked}"
)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 228-228: Unused method argument: fake_uv

(ARG002)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow_scripts/tests/test_mutation_run_mutmut.py` around lines 226 - 247,
Update test_workflow_input_env_does_not_leak_into_mutmut to remove the unused
fake_uv parameter and add pytest.mark.usefixtures("fake_uv") so the fixture's
PATH setup still occurs without triggering Ruff's unused-argument check.

Source: Linters/SAST tools

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

leynos added 3 commits July 27, 2026 14:56
Strip the workflow's INPUT_* variables from the environment before
invoking mutmut run: mutmut executes the caller's test suite in-process
as its baseline, so INPUT_MODULE_PREFIX_STRIP="" set by the caller
leaked into the baseline and broke the module-glob test on every
scheduled run. The affected tests also clear ambient INPUT_* variables
so they exercise the documented defaults rather than the surrounding CI
environment.

Skip pytest test modules (conftest, test_*, *_test, and files under
test/ or tests/ directories) during module-glob translation, and record
a "changed files contain no mutable source" note in the job summary
when the scope short-circuits, so a test-only change window becomes a
graceful no-op instead of aborting mutmut with an empty filter
(leynos/agent-helper-scripts#74).
Note in docs/mutation-mutmut-workflow.md that test modules are excluded
from module-glob translation and that a test-only change window
short-circuits with a "no mutable source" summary note
(leynos/agent-helper-scripts#74).
Replace the three-clause stem conditional in _is_test_file with a
compiled regex and a named directory-name set, resolving the CodeScene
Complex Conditional finding on PR #374.
@lodyai
lodyai Bot force-pushed the fix/mutmut-baseline-hygiene branch from c8a4acf to 1cb03f9 Compare July 27, 2026 12:59
codescene-access[bot]

This comment was marked as outdated.

Record the coverage database regenerated by the post-validation hook so the
tracked artifact matches the completed mutation-runner validation.
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants