vibe/tests/acp/test_read.py
Mathias Gesbert 3f8487f761
v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@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: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-04 18:26:35 +02:00

239 lines
7.5 KiB
Python

from __future__ import annotations
from pathlib import Path
from acp import ReadTextFileResponse
import pytest
from tests.mock.utils import collect_result
from vibe.acp.tools.builtins.read import AcpReadState, Read
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.read import (
DEFAULT_LINE_LIMIT,
ReadArgs,
ReadConfig,
ReadResult,
)
class MockClient:
def __init__(
self,
file_content: str = "line 1\nline 2\nline 3",
read_error: Exception | None = None,
) -> None:
self._file_content = file_content
self._read_error = read_error
self._read_text_file_called = False
self._session_update_called = False
self._last_read_params: dict[str, str | int | None] = {}
async def read_text_file(
self,
path: str,
session_id: str,
limit: int | None = None,
line: int | None = None,
**kwargs,
) -> ReadTextFileResponse:
self._read_text_file_called = True
self._last_read_params = {
"path": path,
"session_id": session_id,
"limit": limit,
"line": line,
}
if self._read_error:
raise self._read_error
content = self._file_content
if line is not None or limit is not None:
lines = content.splitlines(keepends=True)
start_line = (line or 1) - 1
end_line = start_line + limit if limit is not None else len(lines)
lines = lines[start_line:end_line]
content = "".join(lines)
return ReadTextFileResponse(content=content)
async def session_update(self, session_id: str, update, **kwargs) -> None:
self._session_update_called = True
@pytest.fixture
def mock_client() -> MockClient:
return MockClient()
@pytest.fixture
def acp_read_tool(
mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> Read:
monkeypatch.chdir(tmp_path)
config = ReadConfig()
state = AcpReadState.model_construct(
client=mock_client, # type: ignore[arg-type]
session_id="test_session_123",
)
return Read(config_getter=lambda: config, state=state)
class TestAcpReadBasic:
def test_get_name(self) -> None:
assert Read.get_name() == "read"
class TestAcpReadExecution:
@pytest.mark.asyncio
async def test_run_success(
self, acp_read_tool: Read, mock_client: MockClient, tmp_path: Path
) -> None:
test_file = tmp_path / "test_file.txt"
test_file.touch()
args = ReadArgs(file_path=str(test_file))
result = await collect_result(acp_read_tool.run(args))
assert isinstance(result, ReadResult)
assert result.file_path == str(test_file)
assert result.num_lines == 3
assert mock_client._read_text_file_called
params = mock_client._last_read_params
assert params["session_id"] == "test_session_123"
assert params["path"] == str(test_file)
assert params["line"] is None
assert params["limit"] == DEFAULT_LINE_LIMIT + 1
@pytest.mark.asyncio
async def test_run_with_offset(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(
client=mock_client, session_id="test_session"
),
)
args = ReadArgs(file_path=str(test_file), offset=2)
result = await collect_result(tool.run(args))
assert result.num_lines == 2
assert result.start_line == 2
params = mock_client._last_read_params
assert params["line"] == 2
@pytest.mark.asyncio
async def test_run_with_limit(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(
client=mock_client, session_id="test_session"
),
)
args = ReadArgs(file_path=str(test_file), limit=2)
result = await collect_result(tool.run(args))
assert result.num_lines == 2
params = mock_client._last_read_params
assert params["limit"] == 3
@pytest.mark.asyncio
async def test_run_with_offset_and_limit(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt"
test_file.touch()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(
client=mock_client, session_id="test_session"
),
)
args = ReadArgs(file_path=str(test_file), offset=2, limit=1)
result = await collect_result(tool.run(args))
assert result.num_lines == 1
assert result.start_line == 2
params = mock_client._last_read_params
assert params["line"] == 2
assert params["limit"] == 2
@pytest.mark.asyncio
async def test_run_read_error(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
mock_client._read_error = RuntimeError("File not found")
test_file = tmp_path / "test.txt"
test_file.touch()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(
client=mock_client, session_id="test_session"
),
)
args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args))
assert str(exc_info.value) == f"Error reading {test_file}: File not found"
@pytest.mark.asyncio
async def test_run_without_connection(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test.txt"
test_file.touch()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(client=None, session_id="test_session"),
)
args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args))
assert (
str(exc_info.value)
== "Client not available in tool state. This tool can only be used within an ACP session."
)
@pytest.mark.asyncio
async def test_run_without_session_id(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test.txt"
test_file.touch()
mock_client = MockClient()
tool = Read(
config_getter=lambda: ReadConfig(),
state=AcpReadState.model_construct(client=mock_client, session_id=None),
)
args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args))
assert (
str(exc_info.value)
== "Session ID not available in tool state. This tool can only be used within an ACP session."
)