vibe/tests/core/tools/builtins/test_read.py
Guillaume LE GOFF cafb6d4147
v2.15.0 (#773)
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
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: Mistral Vibe <vibe@mistral.ai>
2026-06-12 13:16:04 +02:00

354 lines
10 KiB
Python

from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
from tests.mock.utils import collect_result
from vibe.core.config.harness_files import (
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.read import (
DEFAULT_LINE_LIMIT,
MAX_BYTES,
Read,
ReadArgs,
ReadConfig,
ReadResult,
ReadState,
_add_line_numbers,
)
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.core.types import ToolResultEvent
from vibe.core.utils import VIBE_WARNING_TAG
def _make_read() -> Read:
return Read(config_getter=lambda: ReadConfig(), state=ReadState())
@pytest.mark.asyncio
async def test_reads_entire_small_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "hello.txt").write_text("line one\nline two\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "hello.txt")))
)
assert result.num_lines == 2
assert result.total_lines == 2
assert result.start_line == 1
assert "line one" in result.content
assert "line two" in result.content
@pytest.mark.asyncio
async def test_reads_with_offset_and_limit(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
content = "".join(f"line {i}\n" for i in range(1, 11))
(tmp_path / "f.txt").write_text(content, encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "f.txt"), offset=3, limit=2))
)
assert result.num_lines == 2
assert result.start_line == 3
# Bounded read stops at the limit, so the true total is unknown.
assert result.total_lines is None
assert result.was_truncated is True
assert "line 3" in result.content
assert "line 4" in result.content
assert "line 2" not in result.content
assert "line 5" not in result.content
@pytest.mark.asyncio
async def test_empty_file_returns_warning(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "empty.txt").write_text("", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "empty.txt")))
)
assert result.num_lines == 0
assert VIBE_WARNING_TAG in result.content
assert "empty" in result.content
@pytest.mark.asyncio
async def test_offset_beyond_file_returns_warning(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "short.txt").write_text("one\ntwo\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "short.txt"), offset=100))
)
assert result.num_lines == 0
assert VIBE_WARNING_TAG in result.content
assert "shorter" in result.content
@pytest.mark.asyncio
async def test_exceeds_max_bytes_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
# Create file that generates output exceeding MAX_BYTES
big_line = "x" * 200 + "\n"
lines_needed = (MAX_BYTES // len(big_line)) + 100
(tmp_path / "big.txt").write_text(big_line * lines_needed, encoding="utf-8")
tool = _make_read()
with pytest.raises(ToolError, match="exceeds maximum allowed size"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "big.txt"))))
@pytest.mark.asyncio
async def test_truncated_when_more_lines_than_limit(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
content = "".join(f"line {i}\n" for i in range(1, 101))
(tmp_path / "f.txt").write_text(content, encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "f.txt"), limit=10))
)
assert result.num_lines == 10
assert result.was_truncated is True
assert result.total_lines is None
@pytest.mark.asyncio
async def test_single_oversized_line_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "wide.txt").write_text("x" * (MAX_BYTES + 10), encoding="utf-8")
tool = _make_read()
with pytest.raises(ToolError, match="exceeds maximum allowed size"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "wide.txt"))))
@pytest.mark.asyncio
async def test_file_not_found_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
tool = _make_read()
with pytest.raises(ToolError, match="File not found"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "nope.txt"))))
@pytest.mark.asyncio
async def test_empty_path_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
tool = _make_read()
with pytest.raises(ToolError, match="file_path cannot be empty"):
await collect_result(tool.run(ReadArgs(file_path="")))
@pytest.mark.asyncio
async def test_directory_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "adir").mkdir()
tool = _make_read()
with pytest.raises(ToolError, match="directory"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "adir"))))
@pytest.mark.asyncio
async def test_relative_path_resolved(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "f.txt").write_text("ok\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(tool.run(ReadArgs(file_path="sub/f.txt")))
assert result.num_lines == 1
assert str(tmp_path / "sub" / "f.txt") == result.file_path
def test_line_number_format() -> None:
formatted = _add_line_numbers(["hello", "world"], start=1)
lines = formatted.split("\n")
assert lines[0] == " 1\u2192hello"
assert lines[1] == " 2\u2192world"
def test_default_limit_is_2000() -> None:
assert DEFAULT_LINE_LIMIT == 2000
def test_format_call_display() -> None:
args = ReadArgs(file_path="/some/file.py")
display = Read.format_call_display(args)
assert isinstance(display, ToolCallDisplay)
assert "file.py" in display.summary
def test_format_call_display_with_offset_limit() -> None:
args = ReadArgs(file_path="/some/file.py", offset=10, limit=50)
display = Read.format_call_display(args)
assert "from line 10" in display.summary
assert "limit 50" in display.summary
def test_get_result_display() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=10,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert isinstance(display, ToolResultDisplay)
assert display.success is True
assert "foo.py" in display.message
def test_get_result_display_truncated() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=100,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert "truncated" in display.suffix
def test_get_result_display_truncated_via_flag() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=None,
was_truncated=True,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert "truncated" in display.suffix
@pytest.fixture()
def _setup_manager(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
monkeypatch.setattr(
trusted_folders_manager, "find_trust_root", lambda _: tmp_path.resolve()
)
reset_harness_files_manager()
init_harness_files_manager("user", "project")
yield
reset_harness_files_manager()
@pytest.mark.usefixtures("_setup_manager")
def test_agents_md_injection(tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub instructions", encoding="utf-8")
target = sub / "file.py"
target.write_text("hello", encoding="utf-8")
tool = _make_read()
result = ReadResult(
file_path=str(target), content="hello", num_lines=1, start_line=1, total_lines=1
)
annotation = tool.get_result_extra(result)
assert annotation is not None
assert VIBE_WARNING_TAG in annotation
assert "# Sub instructions" in annotation
@pytest.mark.usefixtures("_setup_manager")
def test_agents_md_deduplicates(tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub", encoding="utf-8")
(sub / "a.py").write_text("a", encoding="utf-8")
(sub / "b.py").write_text("b", encoding="utf-8")
tool = _make_read()
r1 = ReadResult(
file_path=str(sub / "a.py"),
content="a",
num_lines=1,
start_line=1,
total_lines=1,
)
assert tool.get_result_extra(r1) is not None
r2 = ReadResult(
file_path=str(sub / "b.py"),
content="b",
num_lines=1,
start_line=1,
total_lines=1,
)
assert tool.get_result_extra(r2) is None
def test_agents_md_returns_none_when_not_initialized(tmp_path: Path) -> None:
reset_harness_files_manager()
tool = _make_read()
result = ReadResult(
file_path=str(tmp_path / "file.py"),
content="",
num_lines=0,
start_line=1,
total_lines=0,
)
assert tool.get_result_extra(result) is None
reset_harness_files_manager()