01a8acae8a
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.
168 lines
7.3 KiB
Python
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
|