From 2cf4ec37784f65134e4416e2011445495ff2e5eb Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sun, 2 Aug 2026 00:43:47 +0200 Subject: [PATCH] grinder: the diff-noise pathspecs need :(glob), or top-level scratch slips through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `:(exclude)**/__pycache__/**` reads as "exclude any __pycache__" and is not. Without `glob` magic git will not let `**` span ZERO directories, so the pattern hid a nested __pycache__ and missed one at the repo root — which is exactly where pytest puts it for a flat repo. Every grind diff has been carrying a .pyc; @reviewer flagged the symptom on the modeltag grind, and `git diff --stat` against both spellings shows the mechanism. The failure is invisible by inspection, since both spellings say the same thing in English. So tests/test_grind_noise.py builds a real git repo with real scratch and diffs it with the real pathspec list: red on the old spelling for the top-level case only, green on the new one, ~30 ms, no model. NOISE moved to module level to be testable at all. Also normalised pytest-of-* and .pytest_cache to the same **/…/** + glob form; they were top-level-only patterns and would have missed a nested occurrence. --- bin/bullpen-grinder | 18 +++++++--- tests/test_grind_noise.py | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/test_grind_noise.py diff --git a/bin/bullpen-grinder b/bin/bullpen-grinder index 1b334ff..e408e45 100755 --- a/bin/bullpen-grinder +++ b/bin/bullpen-grinder @@ -32,6 +32,19 @@ TIERS = os.environ.get("GRIND_TIERS", "[local] deepseek-v4-flash-dspark").split # [free] openai/gpt-oss-120b which bills a fraction of a cent despite the [free] label) and # previously verified to route + emit FILE: blocks correctly. A stuck floor model now actually # has somewhere to escalate to instead of ESCALATE_AT being dead code. +# The coordinator is asked to APPLY a grind's diff, so test scratch must never appear in it. +# (The sic EPIPE grind returned 12 pytest tmp files alongside the 2 real source hunks.) +# +# `glob` magic is REQUIRED, not decoration: without it git treats `**` as an ordinary +# wildcard that will not span "zero directories", so `**/__pycache__/**` excluded a NESTED +# __pycache__ but not one at the repo root — which is exactly where pytest puts it for a +# flat repo. Every grind diff carried a .pyc because of it (@reviewer flagged the symptom +# 2026-08-02; measured with git diff --stat against both spellings). +# Module-level so tests/test_grind_noise.py can check it without running a grind. +NOISE = [":(exclude,glob)**/pytest-of-*/**", + ":(exclude,glob)**/__pycache__/**", + ":(exclude,glob)**/.pytest_cache/**"] + PY_TIERS = os.environ.get("PY_GRIND_TIERS", "[local] deepseek-v4-flash-dspark,[free] qwen/qwen3-coder") MAX_ITERS = 10 # total model attempts across all tiers ESCALATE_AT = 4 # attempts on a tier without progress before escalating @@ -375,11 +388,6 @@ def grind(task, test_rel, targets): # whether escalation was EARNED, not just on whether it reached green. escalations = [] - # The coordinator is asked to APPLY this diff, so test scratch must never appear in it. - # (The sic EPIPE grind returned 12 pytest tmp files alongside the 2 real source hunks.) - NOISE = [":(exclude)pytest-of-*", ":(exclude)**/__pycache__/**", - ":(exclude).pytest_cache/**"] - def _model_tag(b): # #118: name the COST TIER, not just the model. The gateway may fail over, and # a same-model free->paid escalation used to be invisible here. bullpen_modeltag diff --git a/tests/test_grind_noise.py b/tests/test_grind_noise.py new file mode 100644 index 0000000..e9944b1 --- /dev/null +++ b/tests/test_grind_noise.py @@ -0,0 +1,69 @@ +"""A grind's diff is meant to be APPLIED, so build scratch must never appear in it. + +The exclusion list looked right and did not work: `:(exclude)**/__pycache__/**` without +`glob` magic does not span "zero directories", so it hid a NESTED __pycache__ but not one +at the repo root — which is precisely where pytest puts it for a flat repo. Every grind +diff therefore carried a .pyc (@reviewer, 2026-08-02). + +That is invisible by inspection: both spellings read as "exclude any __pycache__". So the +check is a real git repo with a real top-level __pycache__, diffed with the real pathspec +list — no model, no network, ~30 ms. +""" +import os +import subprocess +import sys + +import pytest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(REPO, "lib")) + + +def _noise(): + """The pathspec list as bullpen-grinder actually defines it (it has no .py suffix, + so it cannot simply be imported).""" + ns = {} + src = open(os.path.join(REPO, "bin", "bullpen-grinder"), encoding="utf-8").read() + for line in src.splitlines(): + if line.startswith("NOISE = ["): + block = src[src.index(line):] + block = block[:block.index("]") + 1] + exec(block, ns) # noqa: S102 — a literal list from our own repo + return ns["NOISE"] + raise AssertionError("bin/bullpen-grinder no longer defines NOISE at module level") + + +def _git(cwd, *args): + return subprocess.run(["git", "-C", cwd, *args], capture_output=True, text=True) + + +@pytest.fixture +def repo(tmp_path): + d = str(tmp_path) + _git(d, "init", "-q") + _git(d, "config", "user.email", "t@example.invalid") + _git(d, "config", "user.name", "t") + open(os.path.join(d, "calc.py"), "w").write("def add(a, b):\n return a - b\n") + _git(d, "add", "-A") + _git(d, "commit", "-q", "-m", "baseline") + return d + + +@pytest.mark.parametrize("scratch", [ + "__pycache__/calc.cpython-314.pyc", # top level — the case that regressed + "pkg/__pycache__/calc.cpython-314.pyc", # nested — the case that already worked + ".pytest_cache/v/cache/nodeids", + "pytest-of-runner/pytest-0/junk.txt", +]) +def test_scratch_never_reaches_the_diff(repo, scratch): + path = os.path.join(repo, scratch) + os.makedirs(os.path.dirname(path), exist_ok=True) + open(path, "wb").write(b"\x00binary-ish\n") + open(os.path.join(repo, "calc.py"), "w").write("def add(a, b):\n return a + b\n") + _git(repo, "add", "-A") + _git(repo, "commit", "-q", "-m", "grind") + + out = _git(repo, "diff", "--stat", "HEAD~1", "HEAD", "--", ".", *_noise()).stdout + assert scratch.split("/")[0] not in out, ( + f"{scratch} reached the diff — the pathspec list does not exclude it:\n{out}") + assert "calc.py" in out, f"the real change vanished along with the scratch:\n{out}"