Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai>
Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-Authored-By: Clément Siriex <clement.sirieix@mistral.ai>
Co-Authored-By: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-Authored-By: Thaddee Tyl <thaddee.tyl@gmail.com>
Co-Authored-By: David Brochart <david.brochart@gmail.com>
Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com>
Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com>
Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
Mathias Gesbert 2026-01-27 16:39:30 +01:00 committed by Mathias Gesbert
parent 79f215d91c
commit d33db9fff8
217 changed files with 16911 additions and 4305 deletions

View file

@ -4,6 +4,7 @@ import shutil
import pytest
from tests.mock.utils import collect_result
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.grep import (
Grep,
@ -15,13 +16,15 @@ from vibe.core.tools.builtins.grep import (
@pytest.fixture
def grep(tmp_path):
config = GrepToolConfig(workdir=tmp_path)
def grep(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
config = GrepToolConfig()
return Grep(config=config, state=GrepState())
@pytest.fixture
def grep_gnu_only(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
original_which = shutil.which
def mock_which(cmd):
@ -30,7 +33,7 @@ def grep_gnu_only(tmp_path, monkeypatch):
return original_which(cmd)
monkeypatch.setattr("shutil.which", mock_which)
config = GrepToolConfig(workdir=tmp_path)
config = GrepToolConfig()
return Grep(config=config, state=GrepState())
@ -66,7 +69,7 @@ def test_raises_error_if_no_grep_available(grep, monkeypatch):
async def test_finds_pattern_in_file(grep, tmp_path):
(tmp_path / "test.py").write_text("def hello():\n print('world')\n")
result = await grep.run(GrepArgs(pattern="hello"))
result = await collect_result(grep.run(GrepArgs(pattern="hello")))
assert result.match_count == 1
assert "hello" in result.matches
@ -78,7 +81,7 @@ async def test_finds_pattern_in_file(grep, tmp_path):
async def test_finds_multiple_matches(grep, tmp_path):
(tmp_path / "test.py").write_text("foo\nbar\nfoo\nbaz\nfoo\n")
result = await grep.run(GrepArgs(pattern="foo"))
result = await collect_result(grep.run(GrepArgs(pattern="foo")))
assert result.match_count == 3
assert result.matches.count("foo") == 3
@ -89,7 +92,7 @@ async def test_finds_multiple_matches(grep, tmp_path):
async def test_returns_empty_on_no_matches(grep, tmp_path):
(tmp_path / "test.py").write_text("def hello():\n pass\n")
result = await grep.run(GrepArgs(pattern="nonexistent"))
result = await collect_result(grep.run(GrepArgs(pattern="nonexistent")))
assert result.match_count == 0
assert result.matches == ""
@ -99,7 +102,7 @@ async def test_returns_empty_on_no_matches(grep, tmp_path):
@pytest.mark.asyncio
async def test_fails_with_empty_pattern(grep):
with pytest.raises(ToolError) as err:
await grep.run(GrepArgs(pattern=""))
await collect_result(grep.run(GrepArgs(pattern="")))
assert "Empty search pattern" in str(err.value)
@ -107,7 +110,7 @@ async def test_fails_with_empty_pattern(grep):
@pytest.mark.asyncio
async def test_fails_with_nonexistent_path(grep):
with pytest.raises(ToolError) as err:
await grep.run(GrepArgs(pattern="test", path="nonexistent"))
await collect_result(grep.run(GrepArgs(pattern="test", path="nonexistent")))
assert "Path does not exist" in str(err.value)
@ -119,7 +122,7 @@ async def test_searches_in_specific_path(grep, tmp_path):
(subdir / "test.py").write_text("match here\n")
(tmp_path / "other.py").write_text("match here too\n")
result = await grep.run(GrepArgs(pattern="match", path="subdir"))
result = await collect_result(grep.run(GrepArgs(pattern="match", path="subdir")))
assert result.match_count == 1
assert "subdir" in result.matches and "test.py" in result.matches
@ -130,19 +133,20 @@ async def test_searches_in_specific_path(grep, tmp_path):
async def test_truncates_to_max_matches(grep, tmp_path):
(tmp_path / "test.py").write_text("\n".join(f"line {i}" for i in range(200)))
result = await grep.run(GrepArgs(pattern="line", max_matches=50))
result = await collect_result(grep.run(GrepArgs(pattern="line", max_matches=50)))
assert result.match_count == 50
assert result.was_truncated
@pytest.mark.asyncio
async def test_truncates_to_max_output_bytes(grep, tmp_path):
config = GrepToolConfig(workdir=tmp_path, max_output_bytes=100)
async def test_truncates_to_max_output_bytes(grep, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
config = GrepToolConfig(max_output_bytes=100)
grep_tool = Grep(config=config, state=GrepState())
(tmp_path / "test.py").write_text("\n".join("x" * 100 for _ in range(10)))
result = await grep_tool.run(GrepArgs(pattern="x"))
result = await collect_result(grep_tool.run(GrepArgs(pattern="x")))
assert len(result.matches) <= 100
assert result.was_truncated
@ -155,7 +159,7 @@ async def test_respects_default_ignore_patterns(grep, tmp_path):
node_modules.mkdir()
(node_modules / "excluded.js").write_text("match\n")
result = await grep.run(GrepArgs(pattern="match"))
result = await collect_result(grep.run(GrepArgs(pattern="match")))
assert "included.py" in result.matches
assert "excluded.js" not in result.matches
@ -170,7 +174,7 @@ async def test_respects_vibeignore_file(grep, tmp_path):
(tmp_path / "excluded.tmp").write_text("match\n")
(tmp_path / "included.py").write_text("match\n")
result = await grep.run(GrepArgs(pattern="match"))
result = await collect_result(grep.run(GrepArgs(pattern="match")))
assert "included.py" in result.matches
assert "excluded.py" not in result.matches
@ -182,7 +186,7 @@ async def test_ignores_comments_in_vibeignore(grep, tmp_path):
(tmp_path / ".vibeignore").write_text("# comment\npattern/\n# another comment\n")
(tmp_path / "file.py").write_text("match\n")
result = await grep.run(GrepArgs(pattern="match"))
result = await collect_result(grep.run(GrepArgs(pattern="match")))
assert result.match_count >= 1
@ -191,20 +195,21 @@ async def test_ignores_comments_in_vibeignore(grep, tmp_path):
async def test_tracks_search_history(grep, tmp_path):
(tmp_path / "test.py").write_text("content\n")
await grep.run(GrepArgs(pattern="first"))
await grep.run(GrepArgs(pattern="second"))
await grep.run(GrepArgs(pattern="third"))
await collect_result(grep.run(GrepArgs(pattern="first")))
await collect_result(grep.run(GrepArgs(pattern="second")))
await collect_result(grep.run(GrepArgs(pattern="third")))
assert grep.state.search_history == ["first", "second", "third"]
@pytest.mark.asyncio
async def test_uses_effective_workdir(tmp_path):
config = GrepToolConfig(workdir=tmp_path)
async def test_uses_effective_workdir(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
config = GrepToolConfig()
grep_tool = Grep(config=config, state=GrepState())
(tmp_path / "test.py").write_text("match\n")
result = await grep_tool.run(GrepArgs(pattern="match", path="."))
result = await collect_result(grep_tool.run(GrepArgs(pattern="match", path=".")))
assert result.match_count == 1
assert "test.py" in result.matches
@ -216,7 +221,7 @@ class TestGnuGrepBackend:
async def test_finds_pattern_in_file(self, grep_gnu_only, tmp_path):
(tmp_path / "test.py").write_text("def hello():\n print('world')\n")
result = await grep_gnu_only.run(GrepArgs(pattern="hello"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="hello")))
assert result.match_count == 1
assert "hello" in result.matches
@ -226,7 +231,7 @@ class TestGnuGrepBackend:
async def test_finds_multiple_matches(self, grep_gnu_only, tmp_path):
(tmp_path / "test.py").write_text("foo\nbar\nfoo\nbaz\nfoo\n")
result = await grep_gnu_only.run(GrepArgs(pattern="foo"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="foo")))
assert result.match_count == 3
assert result.matches.count("foo") == 3
@ -235,7 +240,9 @@ class TestGnuGrepBackend:
async def test_returns_empty_on_no_matches(self, grep_gnu_only, tmp_path):
(tmp_path / "test.py").write_text("def hello():\n pass\n")
result = await grep_gnu_only.run(GrepArgs(pattern="nonexistent"))
result = await collect_result(
grep_gnu_only.run(GrepArgs(pattern="nonexistent"))
)
assert result.match_count == 0
assert result.matches == ""
@ -246,7 +253,7 @@ class TestGnuGrepBackend:
):
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
result = await grep_gnu_only.run(GrepArgs(pattern="hello"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="hello")))
assert result.match_count == 3
@ -254,7 +261,7 @@ class TestGnuGrepBackend:
async def test_case_sensitive_for_mixed_case_pattern(self, grep_gnu_only, tmp_path):
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
result = await grep_gnu_only.run(GrepArgs(pattern="Hello"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="Hello")))
assert result.match_count == 1
@ -265,7 +272,7 @@ class TestGnuGrepBackend:
node_modules.mkdir()
(node_modules / "excluded.js").write_text("match\n")
result = await grep_gnu_only.run(GrepArgs(pattern="match"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="match")))
assert "included.py" in result.matches
assert "excluded.js" not in result.matches
@ -277,7 +284,9 @@ class TestGnuGrepBackend:
(subdir / "test.py").write_text("match here\n")
(tmp_path / "other.py").write_text("match here too\n")
result = await grep_gnu_only.run(GrepArgs(pattern="match", path="subdir"))
result = await collect_result(
grep_gnu_only.run(GrepArgs(pattern="match", path="subdir"))
)
assert result.match_count == 1
assert "other.py" not in result.matches
@ -291,7 +300,7 @@ class TestGnuGrepBackend:
(tmp_path / "excluded.tmp").write_text("match\n")
(tmp_path / "included.py").write_text("match\n")
result = await grep_gnu_only.run(GrepArgs(pattern="match"))
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="match")))
assert "included.py" in result.matches
assert "excluded.py" not in result.matches
@ -301,7 +310,9 @@ class TestGnuGrepBackend:
async def test_truncates_to_max_matches(self, grep_gnu_only, tmp_path):
(tmp_path / "test.py").write_text("\n".join(f"line {i}" for i in range(200)))
result = await grep_gnu_only.run(GrepArgs(pattern="line", max_matches=50))
result = await collect_result(
grep_gnu_only.run(GrepArgs(pattern="line", max_matches=50))
)
assert result.match_count == 50
assert result.was_truncated
@ -313,7 +324,7 @@ class TestRipgrepBackend:
async def test_smart_case_lowercase_pattern(self, grep, tmp_path):
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
result = await grep.run(GrepArgs(pattern="hello"))
result = await collect_result(grep.run(GrepArgs(pattern="hello")))
assert result.match_count == 3
@ -321,7 +332,7 @@ class TestRipgrepBackend:
async def test_smart_case_mixed_case_pattern(self, grep, tmp_path):
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
result = await grep.run(GrepArgs(pattern="Hello"))
result = await collect_result(grep.run(GrepArgs(pattern="Hello")))
assert result.match_count == 1
@ -336,12 +347,12 @@ class TestRipgrepBackend:
(ignored_dir / "file.py").write_text("match\n")
(tmp_path / "included.py").write_text("match\n")
result_with_ignore = await grep.run(GrepArgs(pattern="match"))
result_with_ignore = await collect_result(grep.run(GrepArgs(pattern="match")))
assert "included.py" in result_with_ignore.matches
assert "ignored_by_rg" not in result_with_ignore.matches
result_without_ignore = await grep.run(
GrepArgs(pattern="match", use_default_ignore=False)
result_without_ignore = await collect_result(
grep.run(GrepArgs(pattern="match", use_default_ignore=False))
)
assert "included.py" in result_without_ignore.matches
assert "ignored_by_rg/file.py" in result_without_ignore.matches