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>
This commit is contained in:
parent
ad0d5c9520
commit
3f8487f761
197 changed files with 10819 additions and 2830 deletions
230
tests/core/tools/builtins/test_edit.py
Normal file
230
tests/core/tools/builtins/test_edit.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.mock.utils import collect_result
|
||||
from vibe.core.tools.base import BaseToolState, ToolError
|
||||
from vibe.core.tools.builtins.edit import Edit, EditArgs, EditConfig, EditResult
|
||||
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay
|
||||
from vibe.core.types import ToolResultEvent
|
||||
|
||||
|
||||
def _make_edit() -> Edit:
|
||||
return Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
|
||||
|
||||
|
||||
def _write(tmp_path: Path, name: str, content: str) -> Path:
|
||||
p = tmp_path / name
|
||||
p.write_text(content)
|
||||
return p
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exact_match_replaces(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "hello world\n")
|
||||
edit = _make_edit()
|
||||
|
||||
result = await collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="hello", new_string="goodbye"))
|
||||
)
|
||||
|
||||
assert (tmp_path / "f.txt").read_text() == "goodbye world\n"
|
||||
assert result.file == str(tmp_path / "f.txt")
|
||||
assert result.message == "The file has been updated successfully."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_replace_all(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "aaa bbb aaa\n")
|
||||
edit = _make_edit()
|
||||
|
||||
result = await collect_result(
|
||||
edit.run(
|
||||
EditArgs(
|
||||
file_path="f.txt", old_string="aaa", new_string="ccc", replace_all=True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert (tmp_path / "f.txt").read_text() == "ccc bbb ccc\n"
|
||||
assert result.message == (
|
||||
"The file has been updated. All occurrences were successfully replaced"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_found_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "hello world\n")
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="String to replace not found in file"):
|
||||
await collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="missing", new_string="x"))
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_matches_without_replace_all_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "aaa bbb aaa\n")
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="Found 2 matches"):
|
||||
await collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="aaa", new_string="ccc"))
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_old_equals_new_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "hello\n")
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="No changes to make"):
|
||||
await collect_result(
|
||||
edit.run(
|
||||
EditArgs(file_path="f.txt", old_string="hello", new_string="hello")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_old_string_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "hello\n")
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="old_string cannot be empty"):
|
||||
await collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="", new_string="x"))
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_not_found_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="File does not exist"):
|
||||
await collect_result(
|
||||
edit.run(
|
||||
EditArgs(
|
||||
file_path="/nonexistent/file.py", old_string="x", new_string="y"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_file_path_raises(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
edit = _make_edit()
|
||||
|
||||
with pytest.raises(ToolError, match="File path cannot be empty"):
|
||||
await collect_result(
|
||||
edit.run(EditArgs(file_path="", old_string="x", new_string="y"))
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deletion_removes_exact_string(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "line1\nline2\nline3\n")
|
||||
edit = _make_edit()
|
||||
|
||||
await collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="line2\n", new_string=""))
|
||||
)
|
||||
|
||||
assert (tmp_path / "f.txt").read_text() == "line1\nline3\n"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parallel_edits_same_file_all_land(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
_write(tmp_path, "f.txt", "A\nB\nC\nD\n")
|
||||
edit = _make_edit()
|
||||
|
||||
await asyncio.gather(
|
||||
collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="A", new_string="A1"))
|
||||
),
|
||||
collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="B", new_string="B1"))
|
||||
),
|
||||
collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="C", new_string="C1"))
|
||||
),
|
||||
collect_result(
|
||||
edit.run(EditArgs(file_path="f.txt", old_string="D", new_string="D1"))
|
||||
),
|
||||
)
|
||||
|
||||
assert (tmp_path / "f.txt").read_text() == "A1\nB1\nC1\nD1\n"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_relative_path_resolved_from_cwd(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "sub").mkdir(exist_ok=True)
|
||||
_write(tmp_path / "sub", "f.txt", "old")
|
||||
edit = _make_edit()
|
||||
|
||||
result = await collect_result(
|
||||
edit.run(EditArgs(file_path="sub/f.txt", old_string="old", new_string="new"))
|
||||
)
|
||||
|
||||
assert (tmp_path / "sub" / "f.txt").read_text() == "new"
|
||||
assert result.file == str(tmp_path / "sub" / "f.txt")
|
||||
|
||||
|
||||
def test_format_call_display() -> None:
|
||||
args = EditArgs(file_path="/abs/foo.py", old_string="old", new_string="new")
|
||||
display = Edit.format_call_display(args)
|
||||
|
||||
assert isinstance(display, ToolCallDisplay)
|
||||
assert display.summary == "Editing foo.py"
|
||||
|
||||
|
||||
def test_get_result_display() -> None:
|
||||
result = EditResult(
|
||||
file="/path/to/foo.py",
|
||||
message="The file has been updated successfully.",
|
||||
old_string="old",
|
||||
new_string="new",
|
||||
)
|
||||
event = ToolResultEvent(
|
||||
tool_call_id="test", tool_name="edit", tool_class=None, result=result
|
||||
)
|
||||
display = Edit.get_result_display(event)
|
||||
|
||||
assert isinstance(display, ToolResultDisplay)
|
||||
assert display.success is True
|
||||
assert "foo.py" in display.message
|
||||
355
tests/core/tools/builtins/test_read.py
Normal file
355
tests/core/tools/builtins/test_read.py
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
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 "10 lines" in display.message
|
||||
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.message
|
||||
|
||||
|
||||
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.message
|
||||
|
||||
|
||||
@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()
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
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.builtins.read_file import (
|
||||
ReadFile,
|
||||
ReadFileArgs,
|
||||
ReadFileResult,
|
||||
ReadFileState,
|
||||
ReadFileToolConfig,
|
||||
)
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
from vibe.core.utils import VIBE_WARNING_TAG
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def _setup_manager(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
"""Initialize harness files manager for tests, reset after."""
|
||||
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()
|
||||
|
||||
|
||||
def _make_read_file() -> ReadFile:
|
||||
return ReadFile(config_getter=lambda: ReadFileToolConfig(), state=ReadFileState())
|
||||
|
||||
|
||||
class TestReadFileExecution:
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_large_offset_still_reads_lines(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "large_file.txt"
|
||||
test_file.write_text(
|
||||
"".join(f"line {i}\n" for i in range(200)), encoding="utf-8"
|
||||
)
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=64),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(
|
||||
tool.run(ReadFileArgs(path=str(test_file), offset=50, limit=2))
|
||||
)
|
||||
|
||||
assert result.content == "line 50\nline 51\n"
|
||||
assert result.lines_read == 2
|
||||
assert result.was_truncated
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_marks_truncated_when_max_read_bytes_exceeded(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "big.txt"
|
||||
test_file.write_text(
|
||||
"".join(f"line {i}\n" for i in range(100)), encoding="utf-8"
|
||||
)
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=20),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert result.was_truncated
|
||||
assert result.lines_read > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_eof_reached(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "small.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_limit_matches_remaining_lines(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "exact.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(
|
||||
tool.run(ReadFileArgs(path=str(test_file), limit=10))
|
||||
)
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
|
||||
class TestGetResultExtra:
|
||||
@pytest.mark.usefixtures("_setup_manager")
|
||||
def test_returns_none_when_no_agents_md(self, tmp_path: Path) -> None:
|
||||
sub = tmp_path / "sub"
|
||||
sub.mkdir()
|
||||
target = sub / "file.py"
|
||||
target.write_text("hello", encoding="utf-8")
|
||||
|
||||
tool = _make_read_file()
|
||||
result = ReadFileResult(
|
||||
path=str(target), content="hello", lines_read=1, was_truncated=False
|
||||
)
|
||||
assert tool.get_result_extra(result) is None
|
||||
|
||||
@pytest.mark.usefixtures("_setup_manager")
|
||||
def test_returns_tagged_content_when_agents_md_found(self, 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_file()
|
||||
result = ReadFileResult(
|
||||
path=str(target), content="hello", lines_read=1, was_truncated=False
|
||||
)
|
||||
annotation = tool.get_result_extra(result)
|
||||
assert annotation is not None
|
||||
assert f"<{VIBE_WARNING_TAG}>" in annotation
|
||||
assert f"</{VIBE_WARNING_TAG}>" in annotation
|
||||
assert "# Sub instructions" in annotation
|
||||
assert "project instructions for this directory" in annotation
|
||||
|
||||
@pytest.mark.usefixtures("_setup_manager")
|
||||
def test_deduplicates_across_calls(self, tmp_path: Path) -> None:
|
||||
sub = tmp_path / "sub"
|
||||
sub.mkdir()
|
||||
(sub / "AGENTS.md").write_text("# Sub", encoding="utf-8")
|
||||
file1 = sub / "a.py"
|
||||
file2 = sub / "b.py"
|
||||
file1.write_text("a", encoding="utf-8")
|
||||
file2.write_text("b", encoding="utf-8")
|
||||
|
||||
tool = _make_read_file()
|
||||
|
||||
result1 = ReadFileResult(
|
||||
path=str(file1), content="a", lines_read=1, was_truncated=False
|
||||
)
|
||||
assert tool.get_result_extra(result1) is not None
|
||||
|
||||
# Second call for a different file in the same dir → no duplicate
|
||||
result2 = ReadFileResult(
|
||||
path=str(file2), content="b", lines_read=1, was_truncated=False
|
||||
)
|
||||
assert tool.get_result_extra(result2) is None
|
||||
|
||||
@pytest.mark.usefixtures("_setup_manager")
|
||||
def test_injects_new_dir_after_dedup(self, tmp_path: Path) -> None:
|
||||
sub_a = tmp_path / "a"
|
||||
sub_b = tmp_path / "b"
|
||||
sub_a.mkdir()
|
||||
sub_b.mkdir()
|
||||
(sub_a / "AGENTS.md").write_text("# A", encoding="utf-8")
|
||||
(sub_b / "AGENTS.md").write_text("# B", encoding="utf-8")
|
||||
file_a = sub_a / "f.py"
|
||||
file_b = sub_b / "f.py"
|
||||
file_a.write_text("", encoding="utf-8")
|
||||
file_b.write_text("", encoding="utf-8")
|
||||
|
||||
tool = _make_read_file()
|
||||
|
||||
r1 = ReadFileResult(
|
||||
path=str(file_a), content="", lines_read=0, was_truncated=False
|
||||
)
|
||||
ann1 = tool.get_result_extra(r1)
|
||||
assert ann1 is not None
|
||||
assert "# A" in ann1
|
||||
|
||||
# Different subdirectory → should inject its AGENTS.md
|
||||
r2 = ReadFileResult(
|
||||
path=str(file_b), content="", lines_read=0, was_truncated=False
|
||||
)
|
||||
ann2 = tool.get_result_extra(r2)
|
||||
assert ann2 is not None
|
||||
assert "# B" in ann2
|
||||
|
||||
def test_returns_none_when_manager_not_initialized(self, tmp_path: Path) -> None:
|
||||
reset_harness_files_manager()
|
||||
tool = _make_read_file()
|
||||
result = ReadFileResult(
|
||||
path=str(tmp_path / "file.py"),
|
||||
content="",
|
||||
lines_read=0,
|
||||
was_truncated=False,
|
||||
)
|
||||
assert tool.get_result_extra(result) is None
|
||||
reset_harness_files_manager()
|
||||
Loading…
Add table
Add a link
Reference in a new issue