vibe/tests/core/tools/builtins/test_edit.py
Clément Drouin 6bedf271ce
v2.17.0 (#822)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Hdandria <henri.dandria@mistral.ai>
Co-authored-by: Ivana Dunisijevic <ivana.dunisijevic@mistral.ai>
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Mert Unsal <mert.unsal@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@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: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-19 11:01:24 +02:00

307 lines
9 KiB
Python

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
def test_ui_start_lines_not_part_of_model_contract() -> None:
result = EditResult(file="/x", message="m", old_string="a", new_string="b")
result._ui_start_lines = [42]
assert result.ui_start_lines == [42]
assert "ui_start_lines" not in result.model_dump()
assert "_ui_start_lines" not in result.model_dump()
assert "ui_start_lines" not in result.model_dump_json()
assert "ui_start_lines" not in EditResult.model_fields
assert "ui_start_lines" not in EditResult.model_json_schema().get("properties", {})
assert "ui_start_lines" not in dict(result)
@pytest.mark.asyncio
async def test_ui_start_lines_computed_at_edit_site(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "alpha\nbeta\ngamma\ndelta\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="gamma", new_string="GAMMA"))
)
assert result.ui_start_lines == [3]
@pytest.mark.asyncio
async def test_ui_start_lines_set_for_pure_deletion(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "keep1\nkeep2\nremove\nkeep3\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="remove\n", new_string=""))
)
assert result.ui_start_lines == [3]
@pytest.mark.asyncio
async def test_ui_start_lines_lists_all_occurrences_for_replace_all(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "x\ntgt\ny\ntgt\nz\ntgt\n")
edit = _make_edit()
result = await collect_result(
edit.run(
EditArgs(
file_path="f.txt", old_string="tgt", new_string="TGT", replace_all=True
)
)
)
assert result.ui_start_lines == [2, 4, 6]
@pytest.mark.asyncio
async def test_ui_start_lines_single_entry_without_replace_all(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "a\nuniq\nb\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="uniq", new_string="UNIQ"))
)
assert result.ui_start_lines == [2]