mirror of
https://github.com/mistralai/mistral-vibe
synced 2026-04-25 17:14:55 +02:00
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
342 lines
11 KiB
Python
342 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import shutil
|
|
|
|
import pytest
|
|
|
|
from tests.mock.utils import collect_result
|
|
from vibe.core.tools.base import BaseToolState, ToolError
|
|
from vibe.core.tools.builtins.grep import Grep, GrepArgs, GrepBackend, GrepToolConfig
|
|
|
|
|
|
@pytest.fixture
|
|
def grep(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
config = GrepToolConfig()
|
|
return Grep(config_getter=lambda: config, state=BaseToolState())
|
|
|
|
|
|
@pytest.fixture
|
|
def grep_gnu_only(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
original_which = shutil.which
|
|
|
|
def mock_which(cmd):
|
|
if cmd == "rg":
|
|
return None
|
|
return original_which(cmd)
|
|
|
|
monkeypatch.setattr("shutil.which", mock_which)
|
|
config = GrepToolConfig()
|
|
return Grep(config_getter=lambda: config, state=BaseToolState())
|
|
|
|
|
|
def test_detects_ripgrep_when_available(grep):
|
|
if shutil.which("rg"):
|
|
assert grep._detect_backend() == GrepBackend.RIPGREP
|
|
|
|
|
|
def test_falls_back_to_gnu_grep(grep, monkeypatch):
|
|
original_which = shutil.which
|
|
|
|
def mock_which(cmd):
|
|
if cmd == "rg":
|
|
return None
|
|
return original_which(cmd)
|
|
|
|
monkeypatch.setattr("shutil.which", mock_which)
|
|
|
|
if shutil.which("grep"):
|
|
assert grep._detect_backend() == GrepBackend.GNU_GREP
|
|
|
|
|
|
def test_raises_error_if_no_grep_available(grep, monkeypatch):
|
|
monkeypatch.setattr("shutil.which", lambda cmd: None)
|
|
|
|
with pytest.raises(ToolError) as err:
|
|
grep._detect_backend()
|
|
|
|
assert "Neither ripgrep (rg) nor grep is installed" in str(err.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finds_pattern_in_file(grep, tmp_path):
|
|
(tmp_path / "test.py").write_text("def hello():\n print('world')\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="hello")))
|
|
|
|
assert result.match_count == 1
|
|
assert "hello" in result.matches
|
|
assert "test.py" in result.matches
|
|
assert not result.was_truncated
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finds_multiple_matches(grep, tmp_path):
|
|
(tmp_path / "test.py").write_text("foo\nbar\nfoo\nbaz\nfoo\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="foo")))
|
|
|
|
assert result.match_count == 3
|
|
assert result.matches.count("foo") == 3
|
|
assert not result.was_truncated
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_on_no_matches(grep, tmp_path):
|
|
(tmp_path / "test.py").write_text("def hello():\n pass\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="nonexistent")))
|
|
|
|
assert result.match_count == 0
|
|
assert result.matches == ""
|
|
assert not result.was_truncated
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fails_with_empty_pattern(grep):
|
|
with pytest.raises(ToolError) as err:
|
|
await collect_result(grep.run(GrepArgs(pattern="")))
|
|
|
|
assert "Empty search pattern" in str(err.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fails_with_nonexistent_path(grep):
|
|
with pytest.raises(ToolError) as err:
|
|
await collect_result(grep.run(GrepArgs(pattern="test", path="nonexistent")))
|
|
|
|
assert "Path does not exist" in str(err.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_searches_in_specific_path(grep, tmp_path):
|
|
subdir = tmp_path / "subdir"
|
|
subdir.mkdir()
|
|
(subdir / "test.py").write_text("match here\n")
|
|
(tmp_path / "other.py").write_text("match here too\n")
|
|
|
|
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
|
|
assert "other.py" not in result.matches
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
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 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, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
config = GrepToolConfig(max_output_bytes=100)
|
|
grep_tool = Grep(config_getter=lambda: config, state=BaseToolState())
|
|
(tmp_path / "test.py").write_text("\n".join("x" * 100 for _ in range(10)))
|
|
|
|
result = await collect_result(grep_tool.run(GrepArgs(pattern="x")))
|
|
|
|
assert len(result.matches) <= 100
|
|
assert result.was_truncated
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_respects_default_ignore_patterns(grep, tmp_path):
|
|
(tmp_path / "included.py").write_text("match\n")
|
|
node_modules = tmp_path / "node_modules"
|
|
node_modules.mkdir()
|
|
(node_modules / "excluded.js").write_text("match\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="match")))
|
|
|
|
assert "included.py" in result.matches
|
|
assert "excluded.js" not in result.matches
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_respects_vibeignore_file(grep, tmp_path):
|
|
(tmp_path / ".vibeignore").write_text("custom_dir/\n*.tmp\n")
|
|
custom_dir = tmp_path / "custom_dir"
|
|
custom_dir.mkdir()
|
|
(custom_dir / "excluded.py").write_text("match\n")
|
|
(tmp_path / "excluded.tmp").write_text("match\n")
|
|
(tmp_path / "included.py").write_text("match\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="match")))
|
|
|
|
assert "included.py" in result.matches
|
|
assert "excluded.py" not in result.matches
|
|
assert "excluded.tmp" not in result.matches
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
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 collect_result(grep.run(GrepArgs(pattern="match")))
|
|
|
|
assert result.match_count >= 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_uses_effective_workdir(tmp_path, monkeypatch):
|
|
monkeypatch.chdir(tmp_path)
|
|
config = GrepToolConfig()
|
|
grep_tool = Grep(config_getter=lambda: config, state=BaseToolState())
|
|
(tmp_path / "test.py").write_text("match\n")
|
|
|
|
result = await collect_result(grep_tool.run(GrepArgs(pattern="match", path=".")))
|
|
|
|
assert result.match_count == 1
|
|
assert "test.py" in result.matches
|
|
|
|
|
|
@pytest.mark.skipif(not shutil.which("grep"), reason="GNU grep not available")
|
|
class TestGnuGrepBackend:
|
|
@pytest.mark.asyncio
|
|
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 collect_result(grep_gnu_only.run(GrepArgs(pattern="hello")))
|
|
|
|
assert result.match_count == 1
|
|
assert "hello" in result.matches
|
|
assert "test.py" in result.matches
|
|
|
|
@pytest.mark.asyncio
|
|
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 collect_result(grep_gnu_only.run(GrepArgs(pattern="foo")))
|
|
|
|
assert result.match_count == 3
|
|
assert result.matches.count("foo") == 3
|
|
|
|
@pytest.mark.asyncio
|
|
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 collect_result(
|
|
grep_gnu_only.run(GrepArgs(pattern="nonexistent"))
|
|
)
|
|
|
|
assert result.match_count == 0
|
|
assert result.matches == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_case_insensitive_for_lowercase_pattern(
|
|
self, grep_gnu_only, tmp_path
|
|
):
|
|
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
|
|
|
|
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="hello")))
|
|
|
|
assert result.match_count == 3
|
|
|
|
@pytest.mark.asyncio
|
|
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 collect_result(grep_gnu_only.run(GrepArgs(pattern="Hello")))
|
|
|
|
assert result.match_count == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_respects_exclude_patterns(self, grep_gnu_only, tmp_path):
|
|
(tmp_path / "included.py").write_text("match\n")
|
|
node_modules = tmp_path / "node_modules"
|
|
node_modules.mkdir()
|
|
(node_modules / "excluded.js").write_text("match\n")
|
|
|
|
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="match")))
|
|
|
|
assert "included.py" in result.matches
|
|
assert "excluded.js" not in result.matches
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_searches_in_specific_path(self, grep_gnu_only, tmp_path):
|
|
subdir = tmp_path / "subdir"
|
|
subdir.mkdir()
|
|
(subdir / "test.py").write_text("match here\n")
|
|
(tmp_path / "other.py").write_text("match here too\n")
|
|
|
|
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
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_respects_vibeignore_file(self, grep_gnu_only, tmp_path):
|
|
(tmp_path / ".vibeignore").write_text("custom_dir/\n*.tmp\n")
|
|
custom_dir = tmp_path / "custom_dir"
|
|
custom_dir.mkdir()
|
|
(custom_dir / "excluded.py").write_text("match\n")
|
|
(tmp_path / "excluded.tmp").write_text("match\n")
|
|
(tmp_path / "included.py").write_text("match\n")
|
|
|
|
result = await collect_result(grep_gnu_only.run(GrepArgs(pattern="match")))
|
|
|
|
assert "included.py" in result.matches
|
|
assert "excluded.py" not in result.matches
|
|
assert "excluded.tmp" not in result.matches
|
|
|
|
@pytest.mark.asyncio
|
|
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 collect_result(
|
|
grep_gnu_only.run(GrepArgs(pattern="line", max_matches=50))
|
|
)
|
|
|
|
assert result.match_count == 50
|
|
assert result.was_truncated
|
|
|
|
|
|
@pytest.mark.skipif(not shutil.which("rg"), reason="ripgrep not available")
|
|
class TestRipgrepBackend:
|
|
@pytest.mark.asyncio
|
|
async def test_smart_case_lowercase_pattern(self, grep, tmp_path):
|
|
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="hello")))
|
|
|
|
assert result.match_count == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_smart_case_mixed_case_pattern(self, grep, tmp_path):
|
|
(tmp_path / "test.py").write_text("Hello\nHELLO\nhello\n")
|
|
|
|
result = await collect_result(grep.run(GrepArgs(pattern="Hello")))
|
|
|
|
assert result.match_count == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_searches_ignored_files_when_use_default_ignore_false(
|
|
self, grep, tmp_path
|
|
):
|
|
(tmp_path / ".ignore").write_text("ignored_by_rg/\n")
|
|
|
|
ignored_dir = tmp_path / "ignored_by_rg"
|
|
ignored_dir.mkdir()
|
|
(ignored_dir / "file.py").write_text("match\n")
|
|
(tmp_path / "included.py").write_text("match\n")
|
|
|
|
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 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
|