Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions tests/tools/test_search_zero_match_and_multipath.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ def test_case_mismatch_gets_hint(self, proj):
assert r["total_count"] == 0
assert "case-insensitive" in r.get("warning", "")

def test_case_mismatch_hint_names_the_files(self, proj):
# The probe already ran the -i search; it must hand over the paths,
# not just a count (issue #80522: hint-only sent weak models into
# 5-search casing-variant spirals — +6 turns measured on the A/B eval).
r = json.loads(search_tool("token_alpha", path=str(proj / "proj"), task_id="t-zm"))
w = r.get("warning", "")
assert "a.py" in w and "b.py" in w

def test_regex_metachar_literal_hint(self, proj):
d = proj / "proj"
(d / "meta.py").write_text("result = lookup[key+1]\n")
r = json.loads(search_tool("lookup[key+1]", path=str(d), task_id="t-zm"))
assert r["total_count"] == 0
assert "literal match" in r.get("warning", "")
assert "meta.py" in r.get("warning", "")

def test_true_zero_match_no_hint(self, proj):
r = json.loads(search_tool("zzz_totally_absent_zzz", path=str(proj / "proj"), task_id="t-zm"))
Expand All @@ -46,6 +55,17 @@ def test_hidden_only_match_gets_hint(self, proj):
r = json.loads(search_tool("HIDDEN_ONLY_TOKEN", path=str(d), task_id="t-zm"))
assert r["total_count"] == 0
assert "hidden or gitignored" in r.get("warning", "")
# Same class as the casing probe: the path must be in the hint.
assert "conf.cfg" in r.get("warning", "")

def test_probe_path_list_is_capped(self, proj):
d = proj / "proj"
for i in range(8):
(d / f"cap{i}.txt").write_text("capped_case_token = 1\n")
r = json.loads(search_tool("CAPPED_CASE_TOKEN", path=str(d), task_id="t-zm"))
w = r.get("warning", "")
assert "case-insensitive" in w
assert "+3 more" in w # 8 files, 5 shown

def test_matching_search_unaffected(self, proj):
r = json.loads(search_tool("TOKEN_ALPHA", path=str(proj / "proj"), task_id="t-zm"))
Expand Down
49 changes: 26 additions & 23 deletions tools/file_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2308,24 +2308,36 @@ def _zero_match_probe(self, pattern: str, path: str,
"""
if not self._has_command('rg'):
return None

def _tally(stdout: str):
"""Parse ``path:count`` lines from rg --count-matches."""
total = 0
per_file = []
for line in (stdout or "").strip().splitlines():
p, _sep, n = line.rpartition(":")
if n.isdigit():
total += int(n)
per_file.append(p)
return total, per_file

def _paths_note(per_file, cap: int = 5) -> str:
shown = ", ".join(per_file[:cap])
extra = len(per_file) - cap
return shown + (f" (+{extra} more)" if extra > 0 else "")

glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else ""
probe = self._exec(
f"rg -i --count-matches{glob_expr} "
f"{self._escape_shell_arg(pattern)} {self._escape_shell_arg(path)} "
f"2>/dev/null | head -50",
timeout=30,
)
ci_total = 0
ci_files = 0
for line in (probe.stdout or "").strip().splitlines():
_p, _sep, n = line.rpartition(":")
if n.isdigit():
ci_total += int(n)
ci_files += 1
ci_total, ci_paths = _tally(probe.stdout)
if ci_total > 0:
return (
f"0 exact matches, but {ci_total} case-insensitive match(es) "
f"in {ci_files} file(s) — the pattern's casing may be wrong."
f"in {len(ci_paths)} file(s): {_paths_note(ci_paths)} — "
"the pattern's casing may be wrong."
)
# Hidden/ignored probe: rg skips dotdirs and .gitignore'd files by
# default. When the pattern exists only there, say so instead of
Expand All @@ -2337,18 +2349,12 @@ def _zero_match_probe(self, pattern: str, path: str,
f"2>/dev/null | head -50",
timeout=30,
)
h_total = 0
h_files = 0
for line in (hidden.stdout or "").strip().splitlines():
_p, _sep, n = line.rpartition(":")
if n.isdigit():
h_total += int(n)
h_files += 1
h_total, h_paths = _tally(hidden.stdout)
if h_total > 0:
return (
f"0 matches in visible files, but {h_total} match(es) in "
f"{h_files} hidden or gitignored file(s) — these are excluded "
"by default. Search the hidden path explicitly to include them."
f"{len(h_paths)} hidden or gitignored file(s): "
f"{_paths_note(h_paths)} — these are excluded by default."
)
if re.search(r"[.\[\](){}?*+^$\\|]", pattern):
fixed = self._exec(
Expand All @@ -2357,14 +2363,11 @@ def _zero_match_probe(self, pattern: str, path: str,
f"2>/dev/null | head -50",
timeout=30,
)
f_total = sum(
int(line.rpartition(":")[2])
for line in (fixed.stdout or "").strip().splitlines()
if line.rpartition(":")[2].isdigit()
)
f_total, f_paths = _tally(fixed.stdout)
if f_total > 0:
return (
f"0 regex matches, but {f_total} literal match(es) — the "
f"0 regex matches, but {f_total} literal match(es) in "
f"{len(f_paths)} file(s): {_paths_note(f_paths)} — the "
"pattern contains regex metacharacters that likely need "
"escaping (or pass a simpler substring)."
)
Expand Down
Loading