Skip to content

Commit 06cee53

Browse files
igerberclaude
andcommitted
reviewer-eval: fail closed on malformed empty CLI selectors (CI review P1)
CI Codex flagged a fail-open input gap: malformed selectors silently ran a different matrix than intended. - _resolve_configs dropped empty comma segments, so "A,", ",A", "A,,B" resolved to a narrower-than-typed config set instead of erroring. It now rejects any empty segment. - --strata is nargs="*", and load_cases treated [] as falsy ("no filter"), so a bare --strata (or empty shell expansion) silently ran the WHOLE corpus. load_cases now distinguishes None (no flag -> all strata) from [] (explicit empty -> match nothing), so a bare --strata fails closed (0 cases selected -> non-zero exit). Tests (+2, 80 total): _resolve_configs rejects "A,"/",A"/"A,,B"/""/","/"A, ,B" and still accepts A,B and A; load_cases(None) loads all, load_cases([]) selects nothing, and a single stratum still filters. (Sanity: `run --configs A --strata` now exits "no cases selected".) Verified: ruff clean (changed), black clean, 80 eval tests pass, verify-corpus 2/2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5fb7c30 commit 06cee53

3 files changed

Lines changed: 31 additions & 2 deletions

File tree

tests/test_evals_runtime.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,3 +1614,25 @@ def _fake_mat(case_id, fixture, repo_root, worktrees_root, case_dir="", worktree
16141614
r2.prompt_sha_for(case)
16151615
assert seen[0] != seen[1], "same case, different invocations -> distinct worktree keys"
16161616
assert r1._wt_namespace in seen[0] and r2._wt_namespace in seen[1]
1617+
1618+
1619+
def test_resolve_configs_rejects_empty_selectors():
1620+
"""Malformed comma selectors must fail closed, not silently drop empty segments and
1621+
run a narrower matrix than intended."""
1622+
import run_eval
1623+
1624+
for bad in ("A,", ",A", "A,,B", "", ",", "A, ,B"):
1625+
assert run_eval._resolve_configs(bad) is None, f"{bad!r} must fail closed"
1626+
assert run_eval._resolve_configs("A,B") is not None, "valid A,B still resolves"
1627+
assert run_eval._resolve_configs("A") is not None, "valid single arm still resolves"
1628+
1629+
1630+
def test_load_cases_distinguishes_none_from_empty_strata():
1631+
"""No --strata (None) loads all; a bare --strata ([]) selects NOTHING (fail closed),
1632+
not the whole corpus."""
1633+
from adapters.corpus_loader import CorpusLoader
1634+
1635+
loader = CorpusLoader(str(_EVAL_ROOT / "corpus"), str(_REPO))
1636+
assert len(loader.load_cases(None)) >= 2, "None (no flag) loads all strata"
1637+
assert loader.load_cases([]) == [], "bare --strata ([]) must select nothing"
1638+
assert {c.id for c in loader.load_cases(["s3_negative"])} == {"s3-changelog-prose"}

tools/reviewer-eval/adapters/corpus_loader.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ def load_cases(self, strata: Optional[list[str]] = None) -> list[Case]:
137137
if not os.path.isdir(self.cases_dir):
138138
return cases
139139
for stratum in sorted(os.listdir(self.cases_dir)):
140-
if strata and stratum not in strata:
140+
# Distinguish None (no --strata -> all strata) from [] (a bare --strata with
141+
# no values -> an explicit empty selection that must match NOTHING, so a typo
142+
# / empty shell expansion fails closed instead of silently running everything).
143+
if strata is not None and stratum not in strata:
141144
continue
142145
stratum_dir = os.path.join(self.cases_dir, stratum)
143146
if not os.path.isdir(stratum_dir):

tools/reviewer-eval/run_eval.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ def _resolve_configs(arg: str):
116116
Fail-closed: a typo like ``--configs Z`` (or ``A,Z``) returns None so the
117117
caller can abort, rather than silently running a partial/empty matrix.
118118
"""
119-
requested = [c for c in (arg or "").split(",") if c]
119+
requested = (arg or "").split(",")
120+
# Reject malformed/empty selectors ("", "A,", ",A", "A,,B") rather than silently
121+
# dropping empty segments and running a narrower matrix than the operator intended.
122+
if any(c == "" for c in requested):
123+
return None
120124
# Reject duplicate ids ("A,A"): run identity, artifacts, and bundle columns key
121125
# off config_id, so a repeat would alias both arms onto one identity and collapse
122126
# the comparison rather than running two arms.

0 commit comments

Comments
 (0)