Files
bullpen/tests/test_grinder_ticket.py
Claude (noether) 01a8acae8a grinder: per-ticket model escalation (tiers=/model= in the ticket body)
markus' escalation policy — "if local DeepSeek can't crack one stubborn test, hand that single
task to a stronger model" — had no in-room expression: the tier was only settable via the
service env. A ticket may now carry `tiers=<floor,...,ceiling>` or `model=<m>`, which overrides
PY_TIERS for that one grind. Comma-separated is a ladder the grinder already climbs on stall,
so `tiers=deepseek-v4-flash-dspark,moonshotai/kimi-k2.6` tries the cheap local model first and
escalates to paid Kimi only if it stalls. The regex won't fire on prose ("the models are slow").

First use: TestStdinReadErrorDiagnosedNotSwallowed in cmd/sicd, which DeepSeek missed in 10 tries.
2026-07-23 06:38:06 +02:00

168 lines
7.3 KiB
Python

"""Spec for how the grinder reads a ticket: which repo, and which files it may edit.
Both behaviours below cost real delegation rounds on 2026-07-22/23, and both looked like
"the model is incapable" rather than "the harness is broken":
* @foreman writes the repo as `REPO: boltzmann:/home/mfritsche/src/sic`. The parser only
understood `repo=<path>`, so @py ground the DEFAULT repo, failed to open the test file,
and replied with a raw FileNotFoundError traceback. Twice.
* Target inference matched a hardcoded allowlist of top-level directories. Every repo it
hadn't been taught about inferred NOTHING, so the model was asked to fix a file it was
never shown — the same failure had already been papered over three times by appending
another directory name to the regex.
"""
import importlib.machinery
import importlib.util
import os
import subprocess
import sys
import pytest
GRINDER = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"bin", "bullpen-grinder")
loader = importlib.machinery.SourceFileLoader("grinder_under_test", GRINDER)
spec = importlib.util.spec_from_loader(loader.name, loader)
grinder = importlib.util.module_from_spec(spec)
sys.modules[loader.name] = grinder
loader.exec_module(grinder)
@pytest.mark.parametrize("body,expected", [
("please fix repo=/home/mfritsche/src/sic now", "/home/mfritsche/src/sic"),
("REPO: boltzmann:/home/mfritsche/src/sic\nFILE: gateway/sicd", "/home/mfritsche/src/sic"),
("REPO: /home/mfritsche/src/sic", "/home/mfritsche/src/sic"),
("repo: hertz:/opt/thing\n", "/opt/thing"),
("no repo mentioned at all", None),
])
def test_repo_is_parsed_from_both_ticket_dialects(body, expected):
assert grinder._repo_from_body(body) == expected
def test_host_prefix_is_stripped_but_windowsish_colons_are_not_mangled():
# the grind already runs ON that host, so `host:` is noise; a bare path with no
# `host:/` shape must survive untouched.
assert grinder._repo_from_body("REPO: boltzmann:/srv/x") == "/srv/x"
assert grinder._repo_from_body("repo=/srv/x") == "/srv/x"
@pytest.fixture
def repo(tmp_path):
"""A tiny git repo shaped like sic: a source file the test only names piecewise."""
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
(tmp_path / "gateway").mkdir()
(tmp_path / "gateway" / "sicd").write_text("#!/usr/bin/env python3\n")
(tmp_path / "tests").mkdir()
(tmp_path / "tests" / "test_proto.py").write_text("x")
(tmp_path / "README.md").write_text("docs")
subprocess.run(["git", "-C", str(tmp_path), "add", "-A"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "-c", "user.email=t@t", "-c", "user.name=t",
"commit", "-qm", "init"], check=True)
return tmp_path
def test_target_inferred_from_a_literal_path(repo):
src = 'SICD = "gateway/sicd"\n'
assert grinder._infer_targets(str(repo), "tests/test_proto.py", src) == ["gateway/sicd"]
def test_target_inferred_from_a_piecewise_path(repo):
# the real sic spec: Path(__file__).resolve().parent.parent / "gateway" / "sicd"
src = 'SICD = str(Path(__file__).resolve().parent.parent / "gateway" / "sicd")\n'
assert grinder._infer_targets(str(repo), "tests/test_proto.py", src) == ["gateway/sicd"]
def test_the_test_file_itself_is_never_a_target(repo):
src = 'import tests.test_proto # gateway/sicd\n'
out = grinder._infer_targets(str(repo), "tests/test_proto.py", src)
assert "tests/test_proto.py" not in out
assert all(not t.startswith("tests/") for t in out), "never let the grinder edit the tests"
def test_unrelated_repo_files_are_not_offered(repo):
src = 'SICD = "gateway/sicd"\n'
assert "README.md" not in grinder._infer_targets(str(repo), "tests/test_proto.py", src)
# --- spec resolution. Ticket 739 (2026-07-23) was a precise, well-formed ticket that @py
# bounced solely because it named the failing TESTS but not the FILE. A coordinator should
# not have to satisfy a regex to get work done; resolve it, or fail with something actionable.
@pytest.fixture
def files_on_host(monkeypatch):
"""Stub the git-on-the-grind-host lookup; no sic, no network."""
def setter(listing):
monkeypatch.setattr(grinder, "_repo_test_files", lambda repo: sorted(listing))
return setter
def test_explicit_path_wins(files_on_host):
files_on_host(["tests/other.py"])
assert grinder._resolve_test_rel("run tests/test_sicd_protocol.py now", "/r") == (
"tests/test_sicd_protocol.py", None)
def test_the_739_case_bare_test_names_and_a_single_spec_file(files_on_host):
"""The real ticket: names three failing tests, a repo, and no path at all."""
files_on_host(["tests/test_sicd_protocol.py"])
body = ("Grind the 3 new red tests green in boltzmann:~/src/sic.\n"
"TICKET: repo=/home/mfritsche/src/sic\n"
"1. test_epipe_child_dead_before_payload_write_no_traceback — BrokenPipeError\n"
"2. test_partial_write_payload_not_truncated — got 7 of 37 bytes\n")
path, err = grinder._resolve_test_rel(body, "/home/mfritsche/src/sic")
assert err is None
assert path == "tests/test_sicd_protocol.py"
def test_bare_filename_is_matched_against_the_repo(files_on_host):
files_on_host(["tests/test_a.py", "tests/test_b.py"])
assert grinder._resolve_test_rel("fix test_b.py please", "/r") == ("tests/test_b.py", None)
def test_sole_spec_file_is_used_when_the_ticket_names_none(files_on_host):
files_on_host(["tests/test_only.py"])
assert grinder._resolve_test_rel("make the red tests green", "/r") == ("tests/test_only.py", None)
def test_ambiguity_is_never_guessed_and_the_error_lists_candidates(files_on_host):
files_on_host(["tests/test_a.py", "tests/test_b.py"])
path, err = grinder._resolve_test_rel("make the red tests green", "/r")
assert path is None
assert "test_a.py" in err and "test_b.py" in err, "an error must be actionable"
assert "SPEC:" in err, "tell the coordinator exactly what to add"
def test_named_file_absent_from_repo_is_reported_with_what_does_exist(files_on_host):
files_on_host(["tests/test_real.py"])
path, err = grinder._resolve_test_rel("grind test_imaginary.py", "/r")
assert path is None
assert "test_imaginary.py" in err and "test_real.py" in err
def test_no_tests_at_all_still_points_at_testdesigner(files_on_host):
files_on_host([])
path, err = grinder._resolve_test_rel("grind it", "/r")
assert path is None
assert "@testdesigner" in err, "keep the write-tests-first contract in the refusal"
# --- per-ticket model escalation (markus' "if DeepSeek can't crack one test, hand it to a
# stronger model" policy, made expressible in-room 2026-07-23).
@pytest.mark.parametrize("body,expected", [
("grind this tiers=gpt-oss-120b", "gpt-oss-120b"),
("fix it model=kimi-k3", "kimi-k3"),
("ladder tiers=deepseek-v4-flash-dspark,gpt-oss-120b,kimi-k3",
"deepseek-v4-flash-dspark,gpt-oss-120b,kimi-k3"),
("no override here", None),
("MODELS=gpt-oss-120b case-insensitive", "gpt-oss-120b"),
])
def test_tiers_parsed_from_ticket_body(body, expected):
assert grinder._tiers_from_body(body) == expected
def test_tiers_override_does_not_eat_the_word_models_in_prose():
# a mention like "the models are slow" must not be read as an override
assert grinder._tiers_from_body("the models are slow today") is None