Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Laure Hugo <laure.hugo@mistral.ai> Co-Authored-By: Benjamin Trom <benjamin.trom@mistral.ai> Co-Authored-By: Mathias Gesbert <mathias.gesbert@ext.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: Valentin Berard <val@mistral.ai> Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
240 lines
8 KiB
Python
240 lines
8 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from acp import ReadTextFileRequest, ReadTextFileResponse
|
|
import pytest
|
|
|
|
from vibe.acp.tools.builtins.read_file import AcpReadFileState, ReadFile
|
|
from vibe.core.tools.base import ToolError
|
|
from vibe.core.tools.builtins.read_file import (
|
|
ReadFileArgs,
|
|
ReadFileResult,
|
|
ReadFileToolConfig,
|
|
)
|
|
|
|
|
|
class MockConnection:
|
|
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_request: ReadTextFileRequest | None = None
|
|
|
|
async def readTextFile(self, request: ReadTextFileRequest) -> ReadTextFileResponse:
|
|
self._read_text_file_called = True
|
|
self._last_read_request = request
|
|
|
|
if self._read_error:
|
|
raise self._read_error
|
|
|
|
content = self._file_content
|
|
if request.line is not None or request.limit is not None:
|
|
lines = content.splitlines(keepends=True)
|
|
start_line = (request.line or 1) - 1 # Convert to 0-indexed
|
|
end_line = (
|
|
start_line + request.limit if request.limit is not None else len(lines)
|
|
)
|
|
lines = lines[start_line:end_line]
|
|
content = "".join(lines)
|
|
|
|
return ReadTextFileResponse(content=content)
|
|
|
|
async def sessionUpdate(self, notification) -> None:
|
|
self._session_update_called = True
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_connection() -> MockConnection:
|
|
return MockConnection()
|
|
|
|
|
|
@pytest.fixture
|
|
def acp_read_file_tool(mock_connection: MockConnection, tmp_path: Path) -> ReadFile:
|
|
config = ReadFileToolConfig(workdir=tmp_path)
|
|
state = AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id="test_session_123",
|
|
tool_call_id="test_tool_call_456",
|
|
)
|
|
return ReadFile(config=config, state=state)
|
|
|
|
|
|
class TestAcpReadFileBasic:
|
|
def test_get_name(self) -> None:
|
|
assert ReadFile.get_name() == "read_file"
|
|
|
|
|
|
class TestAcpReadFileExecution:
|
|
@pytest.mark.asyncio
|
|
async def test_run_success(
|
|
self,
|
|
acp_read_file_tool: ReadFile,
|
|
mock_connection: MockConnection,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
test_file = tmp_path / "test_file.txt"
|
|
test_file.touch()
|
|
args = ReadFileArgs(path=str(test_file))
|
|
result = await acp_read_file_tool.run(args)
|
|
|
|
assert isinstance(result, ReadFileResult)
|
|
assert result.path == str(test_file)
|
|
assert result.content == "line 1\nline 2\nline 3"
|
|
assert result.lines_read == 3
|
|
assert mock_connection._read_text_file_called
|
|
assert mock_connection._session_update_called
|
|
|
|
# Verify ReadTextFileRequest was created correctly
|
|
request = mock_connection._last_read_request
|
|
assert request is not None
|
|
assert request.sessionId == "test_session_123"
|
|
assert request.path == str(test_file)
|
|
assert request.line is None # offset=0 means no line specified
|
|
assert request.limit is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_with_offset(
|
|
self, mock_connection: MockConnection, tmp_path: Path
|
|
) -> None:
|
|
test_file = tmp_path / "test_file.txt"
|
|
test_file.touch()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id="test_session",
|
|
tool_call_id="test_call",
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file), offset=1)
|
|
result = await tool.run(args)
|
|
|
|
assert result.lines_read == 2
|
|
assert result.content == "line 2\nline 3"
|
|
|
|
request = mock_connection._last_read_request
|
|
assert request is not None
|
|
assert request.line == 2 # offset=1 means line 2 (1-indexed)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_with_limit(
|
|
self, mock_connection: MockConnection, tmp_path: Path
|
|
) -> None:
|
|
test_file = tmp_path / "test_file.txt"
|
|
test_file.touch()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id="test_session",
|
|
tool_call_id="test_call",
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file), limit=2)
|
|
result = await tool.run(args)
|
|
|
|
assert result.lines_read == 2
|
|
assert result.content == "line 1\nline 2\n"
|
|
|
|
request = mock_connection._last_read_request
|
|
assert request is not None
|
|
assert request.limit == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_with_offset_and_limit(
|
|
self, mock_connection: MockConnection, tmp_path: Path
|
|
) -> None:
|
|
test_file = tmp_path / "test_file.txt"
|
|
test_file.touch()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id="test_session",
|
|
tool_call_id="test_call",
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file), offset=1, limit=1)
|
|
result = await tool.run(args)
|
|
|
|
assert result.lines_read == 1
|
|
assert result.content == "line 2\n"
|
|
|
|
request = mock_connection._last_read_request
|
|
assert request is not None
|
|
assert request.line == 2
|
|
assert request.limit == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_read_error(
|
|
self, mock_connection: MockConnection, tmp_path: Path
|
|
) -> None:
|
|
mock_connection._read_error = RuntimeError("File not found")
|
|
test_file = tmp_path / "test.txt"
|
|
test_file.touch()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id="test_session",
|
|
tool_call_id="test_call",
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file))
|
|
with pytest.raises(ToolError) as exc_info:
|
|
await 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) -> None:
|
|
test_file = tmp_path / "test.txt"
|
|
test_file.touch()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=None, session_id="test_session", tool_call_id="test_call"
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file))
|
|
with pytest.raises(ToolError) as exc_info:
|
|
await tool.run(args)
|
|
|
|
assert (
|
|
str(exc_info.value)
|
|
== "Connection 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) -> None:
|
|
test_file = tmp_path / "test.txt"
|
|
test_file.touch()
|
|
mock_connection = MockConnection()
|
|
tool = ReadFile(
|
|
config=ReadFileToolConfig(workdir=tmp_path),
|
|
state=AcpReadFileState.model_construct(
|
|
connection=mock_connection, # type: ignore[arg-type]
|
|
session_id=None,
|
|
tool_call_id="test_call",
|
|
),
|
|
)
|
|
|
|
args = ReadFileArgs(path=str(test_file))
|
|
with pytest.raises(ToolError) as exc_info:
|
|
await 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."
|
|
)
|