diff --git a/src/fastcontext/agent/tool/grep.py b/src/fastcontext/agent/tool/grep.py index 4ee161f..ffe87e1 100644 --- a/src/fastcontext/agent/tool/grep.py +++ b/src/fastcontext/agent/tool/grep.py @@ -159,7 +159,13 @@ def run_rg(rg_path: str, pattern: str, path: str, **kwargs) -> str: cwd = kwargs.get("cwd", str(Path.cwd())) - output = subprocess.run(command, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace") + timeout = 10 # seconds + try: + output = subprocess.run( + command, cwd=cwd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout + ) + except subprocess.TimeoutExpired: + return f"Grep timed out after {timeout}s" if output.returncode == 0: output_text = ( diff --git a/tests/test_grep_timeout.py b/tests/test_grep_timeout.py new file mode 100644 index 0000000..52b6e0f --- /dev/null +++ b/tests/test_grep_timeout.py @@ -0,0 +1,43 @@ +import asyncio +import json +import subprocess +import tempfile +from pathlib import Path +from unittest import mock + +from fastcontext.agent.tool.grep import GrepTool + + +def test_grep_returns_reminder_on_timeout(): + """run_rg must bound the ripgrep subprocess and surface a system-reminder + instead of hanging / raising when it times out. (The outer asyncio + wait_for cannot interrupt the blocking subprocess.run, so the timeout + must live on the subprocess itself.)""" + grep = GrepTool() + with tempfile.TemporaryDirectory() as cwd: + (Path(cwd) / "a.txt").write_text("MATCH\n", encoding="utf-8") + + with mock.patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="rg", timeout=10)): + out = asyncio.run(grep.call(json.dumps({"pattern": "MATCH", "output_mode": "content"}), cwd=cwd)) + + assert "" in out + assert "timed out" in out + + +def test_grep_passes_timeout_to_subprocess(): + """The subprocess.run call must receive a positive timeout kwarg.""" + grep = GrepTool() + with tempfile.TemporaryDirectory() as cwd: + (Path(cwd) / "a.txt").write_text("MATCH\n", encoding="utf-8") + + real_run = subprocess.run + captured = {} + + def spy(*args, **kwargs): + captured.update(kwargs) + return real_run(*args, **kwargs) + + with mock.patch("subprocess.run", side_effect=spy): + asyncio.run(grep.call(json.dumps({"pattern": "MATCH", "output_mode": "content"}), cwd=cwd)) + + assert captured.get("timeout", 0) and captured["timeout"] > 0