Skip to content
Open
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
10 changes: 7 additions & 3 deletions raven/agent/tools/file_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ class FindTool(_FsTool):
"""Find files by glob pattern, sorted by recency. Pure-Python (pathlib)."""

_DEFAULT_LIMIT = 1000
timeout_seconds = 30.0

@property
def name(self) -> str:
Expand Down Expand Up @@ -413,11 +414,14 @@ async def execute(
# recursively (fd-style), so 'foo.py' finds it at any depth.
glob_expr = pattern if "/" in pattern else f"**/{pattern}"
try:
matches = [
p for p in base.glob(glob_expr) if not any(part in _IGNORE_DIRS for part in p.relative_to(base).parts)
]
return await asyncio.to_thread(self._run_find, base, glob_expr, cap)
except (ValueError, OSError) as e:
return f"Error running find: {e}"

def _run_find(self, base: Path, glob_expr: str, cap: int) -> str:
matches = [
p for p in base.glob(glob_expr) if not any(part in _IGNORE_DIRS for part in p.relative_to(base).parts)
]
if not matches:
return "No files found matching pattern."

Expand Down
48 changes: 48 additions & 0 deletions tests/test_search_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

from __future__ import annotations

import asyncio
import shutil
import time
from pathlib import Path

import pytest

from raven.agent.tools.file_search import FindTool, GrepTool
from raven.agent.tools.registry import ToolRegistry


@pytest.fixture
Expand Down Expand Up @@ -145,3 +148,48 @@ async def test_find_sorted_by_recency(tree: Path):
out = await tool.execute(pattern="*.py")
lines = out.splitlines()
assert lines[0].endswith("util.py")


async def test_find_does_not_block_the_event_loop(tree: Path, monkeypatch):
original_glob = Path.glob

def slow_glob(path: Path, pattern: str):
time.sleep(0.5)
return original_glob(path, pattern)

monkeypatch.setattr(Path, "glob", slow_glob)
tool = FindTool(workspace=tree, allowed_dir=tree)

started = time.monotonic()
search = asyncio.create_task(tool.execute(pattern="*.py"))
await asyncio.sleep(0.02)
event_loop_delay = time.monotonic() - started
result = await search

assert event_loop_delay < 0.25
assert "src/app.py" in result


async def test_find_registry_timeout_can_fire_during_slow_glob(tree: Path, monkeypatch):
original_glob = Path.glob

def slow_glob(path: Path, pattern: str):
time.sleep(0.5)
return original_glob(path, pattern)

monkeypatch.setattr(Path, "glob", slow_glob)
tool = FindTool(workspace=tree, allowed_dir=tree)
tool.timeout_seconds = 0.05
registry = ToolRegistry()
registry.register(tool)

started = time.monotonic()
result = await registry.execute("find", {"pattern": "*.py"})
elapsed = time.monotonic() - started

assert elapsed < 0.25
assert "timed out after" in result


def test_find_declares_a_bounded_timeout():
assert FindTool.timeout_seconds == 30.0