From 9037501b7986bc6ebc4af46966e2fab464ab3d81 Mon Sep 17 00:00:00 2001 From: harness-test Date: Tue, 11 Aug 2026 23:50:50 -0700 Subject: [PATCH] - Adds new Mutmut badge (manually for now) - Fixes test causing Mutmut cron CI failure --- .github/workflows/mutation.yml | 2 +- README.md | 1 + harness/cli.py | 5 +++-- harness/gate.py | 3 ++- harness/tests/test_gate.py | 8 +++++++- mutation-score.json | 6 ++++++ mutation/check_mutmut.py | 6 ++++-- pyproject.toml | 6 ++++-- tests/mutation/test_check_mutmut.py | 16 ++++++++++------ 9 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 mutation-score.json diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index ef5d21b..ae8ba4e 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -44,7 +44,7 @@ jobs: run: uv run --no-sync mutmut export-cicd-stats - name: Upload Mutmut JSON Report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@v4 with: name: mutmut-json-report path: mutants/mutmut-cicd-stats.json diff --git a/README.md b/README.md index 351b198..d0385c0 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ [![](https://img.shields.io/badge/created%20an%20AGI%20by%20mistake-no-3C1)](https://github.com/sebmestrallet/absurd-badges) ![Claude](https://img.shields.io/badge/Claude-D97757?style=for-the-badge&logo=claude&logoColor=white) [![gate](https://github.com/rxdt/loopgate_harness/actions/workflows/ci.yml/badge.svg)](https://github.com/rxdt/loopgate_harness/actions/workflows/ci.yml) +[![mutation](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Frxdt%2Floopgate_harness%2Fmain%2Fmutation-score.json)](https://github.com/rxdt/loopgate_harness/actions/workflows/mutation.yml) diff --git a/harness/cli.py b/harness/cli.py index 240364a..c38ab0f 100644 --- a/harness/cli.py +++ b/harness/cli.py @@ -27,9 +27,9 @@ help="Commands to harness the loops", no_args_is_help=True, add_completion=False, - rich_markup_mode="rich", + rich_markup_mode=None if os.environ.get("RALPH_LOOP") else "rich", ) -console = Console(force_terminal=True) +console = Console(force_terminal=True, color_system=None if os.environ.get("RALPH_LOOP") else "256") REPO_ROOT_STR = str(gates.repo_root) @@ -198,6 +198,7 @@ def cleanup(cwd: Path, name: str | None) -> bool: ".loops.svg", ".github/workflows/publish.yml", "CONTRIBUTING.md", + "LICENSE.md", ): (cwd / file_name).unlink(missing_ok=True) for directory in (cwd / "dist", cwd / "harness" / "tests"): diff --git a/harness/gate.py b/harness/gate.py index 644a757..2d564b2 100644 --- a/harness/gate.py +++ b/harness/gate.py @@ -19,7 +19,8 @@ import typer from rich.console import Console -console = Console(force_terminal=True, color_system="256") +console = Console(force_terminal=True, color_system=None if os.environ.get("RALPH_LOOP") else "256") + try: from preferences.preferences import preferences_violations as prefs except ImportError: # humans can delete preferences.py diff --git a/harness/tests/test_gate.py b/harness/tests/test_gate.py index d25f952..51c76d1 100644 --- a/harness/tests/test_gate.py +++ b/harness/tests/test_gate.py @@ -485,12 +485,18 @@ def test_gate_runs_exactly_what_pyproject_configures( (git_repo / "pyproject.toml").write_text("[project]\nname = 'x'\n", encoding="utf-8") with pytest.raises(KeyError): Gate(git_repo) + expected_repo_root = Path( + subprocess.check_output(["git", "rev-parse", "--show-toplevel"], cwd=REPO_ROOT, text=True).strip() + ) assert ( Path(gate.run_git(["rev-parse", "--show-toplevel"]).strip()), Path(gate.run_git(["rev-parse", "--show-toplevel"], REPO_ROOT).strip()), - ) == (git_repo, REPO_ROOT) + ) == (git_repo, expected_repo_root) monkeypatch.setenv("GIT_DIR", str(git_repo / "no-such-dir")) monkeypatch.delenv("RALPH_LOOP", raising=False) + monkeypatch.setattr( + gate, "console", gate.Console(force_terminal=True, color_system="256", no_color=False) + ) assert Path(gate.run_git(["rev-parse", "--show-toplevel"]).strip()) == git_repo monkeypatch.delenv("GIT_DIR") absent = ["rev-parse", "--verify", "refs/heads/absent"] diff --git a/mutation-score.json b/mutation-score.json new file mode 100644 index 0000000..7a04776 --- /dev/null +++ b/mutation-score.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "label": "mutation", + "message": "83.0%", + "color": "#55ff00" +} diff --git a/mutation/check_mutmut.py b/mutation/check_mutmut.py index 01c964b..831a5ad 100644 --- a/mutation/check_mutmut.py +++ b/mutation/check_mutmut.py @@ -48,6 +48,7 @@ from __future__ import annotations import json +import os from pathlib import Path import typer @@ -55,8 +56,9 @@ from rich.console import Console from rich.table import Table -console = Console(force_terminal=True) -MINIMUM_MUTATION_SCORE = 60.0 +console = Console(force_terminal=True, color_system=None if os.environ.get("RALPH_LOOP") else "256") + +MINIMUM_MUTATION_SCORE = 80.0 JsonDocument = dict[str, object] | list[object] | str | int | float | bool | None diff --git a/pyproject.toml b/pyproject.toml index 15ca0d9..5a2c3ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "harness" version = "0.1.0" requires-python = ">=3.10,<4" dependencies = [ + "mutmut>=3.7.0", "packaging", "tomlkit", "typer", @@ -211,8 +212,9 @@ skip_covered = false # Same behavior for older coverage config rea # ============================================================================== [tool.mutmut] max-children = 2 -source_paths = ["preferences", "src", "mutation"] # what to mutate -also_copy = ["harness", ".githooks"] # not mutated paths, copied for imports +source_paths = ["preferences", "src", "harness"] # what to mutate +also_copy = ["mutation", ".githooks"] # not mutated paths, copied for imports +do_not_mutate = ["harness/tests/*"] # ============================================================================== # Complexipy Configuration diff --git a/tests/mutation/test_check_mutmut.py b/tests/mutation/test_check_mutmut.py index 630b990..026b058 100644 --- a/tests/mutation/test_check_mutmut.py +++ b/tests/mutation/test_check_mutmut.py @@ -11,7 +11,7 @@ import typer from click import unstyle -from mutation.check_mutmut import analyze_mutmut_report +from mutation.check_mutmut import MINIMUM_MUTATION_SCORE, analyze_mutmut_report def test_report_with_timeout_passes_and_renders(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: @@ -40,18 +40,22 @@ def test_report_enforces_threshold(tmp_path: Path, capsys: pytest.CaptureFixture report = tmp_path / "mutmut-cicd-stats.json" report.write_text(json.dumps(data), encoding="utf-8") mutation_score = analyze_mutmut_report(str(report)) - assert 60.0 <= mutation_score < 100.0 + assert MINIMUM_MUTATION_SCORE <= mutation_score < 100.0 - report.write_text(json.dumps({"killed": 3, "timeout": 0, "total": 5, "skipped": 0}), encoding="utf-8") - assert analyze_mutmut_report(str(report)) == pytest.approx(60.0) + passing_report = {"killed": MINIMUM_MUTATION_SCORE, "timeout": 0, "total": 100, "skipped": 0} + report.write_text(json.dumps(passing_report), encoding="utf-8") + assert analyze_mutmut_report(str(report)) == pytest.approx(MINIMUM_MUTATION_SCORE) - report.write_text(json.dumps({"killed": 59, "timeout": 0, "total": 100, "skipped": 0}), encoding="utf-8") + failing_score = MINIMUM_MUTATION_SCORE - 1 + report.write_text( + json.dumps({"killed": failing_score, "timeout": 0, "total": 100, "skipped": 0}), encoding="utf-8" + ) with pytest.raises(typer.Exit) as exc_info: analyze_mutmut_report(str(report)) assert exc_info.value.exit_code == 1 output = " ".join(unstyle(capsys.readouterr().out).split()) - assert "MUTATION SCORE: 59.0" in output + assert f"MUTATION SCORE: {failing_score}" in output def test_missing_report_fails(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: