v2.11.0 (#717)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
f71bfd3b8c
commit
adb1ca74ce
202 changed files with 5828 additions and 1968 deletions
|
|
@ -11,6 +11,7 @@ from vibe.acp.tools.builtins.bash import AcpBashState, Bash
|
|||
from vibe.acp.tools.events import ToolTerminalOpenedEvent
|
||||
from vibe.core.tools.base import InvokeContext, ToolError
|
||||
from vibe.core.tools.builtins.bash import BashArgs, BashResult, BashToolConfig
|
||||
from vibe.core.types import ToolResultEvent
|
||||
|
||||
|
||||
class MockTerminalHandle:
|
||||
|
|
@ -483,3 +484,42 @@ class TestAcpBashCleanup:
|
|||
|
||||
assert result is not None
|
||||
assert result.stdout == "test output"
|
||||
|
||||
|
||||
class TestAcpBashToolResultSessionUpdate:
|
||||
def test_success_reports_completed(self) -> None:
|
||||
event = ToolResultEvent(
|
||||
tool_name="bash",
|
||||
tool_call_id="call_1",
|
||||
tool_class=Bash,
|
||||
result=BashResult(command="echo ok", stdout="ok", stderr="", returncode=0),
|
||||
)
|
||||
|
||||
update = Bash.tool_result_session_update(event)
|
||||
|
||||
assert update is not None
|
||||
assert update.status == "completed"
|
||||
|
||||
def test_user_rejection_reports_failed(self) -> None:
|
||||
event = ToolResultEvent(
|
||||
tool_name="bash",
|
||||
tool_call_id="call_1",
|
||||
tool_class=Bash,
|
||||
skipped=True,
|
||||
skip_reason="User rejected the tool call, provide an alternative plan",
|
||||
)
|
||||
|
||||
update = Bash.tool_result_session_update(event)
|
||||
|
||||
assert update is not None
|
||||
assert update.status == "failed"
|
||||
|
||||
def test_error_reports_failed(self) -> None:
|
||||
event = ToolResultEvent(
|
||||
tool_name="bash", tool_call_id="call_1", tool_class=Bash, error="boom"
|
||||
)
|
||||
|
||||
update = Bash.tool_result_session_update(event)
|
||||
|
||||
assert update is not None
|
||||
assert update.status == "failed"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.10.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
@ -97,7 +97,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.10.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ from vibe.core.tools.builtins.search_replace import (
|
|||
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||
|
||||
|
||||
def _make_block(search: str, replace: str) -> str:
|
||||
head = "<" * 7 + " SEARCH"
|
||||
sep = "=" * 7
|
||||
tail = ">" * 7 + " REPLACE"
|
||||
return f"{head}\n{search}\n{sep}\n{replace}\n{tail}"
|
||||
|
||||
|
||||
class MockClient:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -153,11 +160,74 @@ class TestAcpSearchReplaceExecution:
|
|||
result = await collect_result(tool.run(args))
|
||||
|
||||
assert result.blocks_applied == 1
|
||||
# Should have written the main file and the backup
|
||||
assert len(mock_client._write_calls) >= 1
|
||||
# Check if backup was written (it should be written to .bak file)
|
||||
assert sum(w["path"].endswith(".bak") for w in mock_client._write_calls) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"])
|
||||
async def test_run_preserves_line_endings(
|
||||
self,
|
||||
newline: str,
|
||||
mock_client: MockClient,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
mock_client._file_content = newline.join([
|
||||
"original line 1",
|
||||
"original line 2",
|
||||
"original line 3",
|
||||
])
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file),
|
||||
content=_make_block("original line 2", "modified line 2"),
|
||||
)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert mock_client._last_write_params["content"] == newline.join([
|
||||
"original line 1",
|
||||
"modified line 2",
|
||||
"original line 3",
|
||||
])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_backup_preserves_crlf_byte_for_byte(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
original = "original line 1\r\noriginal line 2\r\noriginal line 3"
|
||||
mock_client._file_content = original
|
||||
|
||||
tool = SearchReplace(
|
||||
config_getter=lambda: SearchReplaceConfig(create_backup=True),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
args = SearchReplaceArgs(
|
||||
file_path=str(test_file),
|
||||
content=_make_block("original line 1", "modified line 1"),
|
||||
)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
backup_calls = [
|
||||
w for w in mock_client._write_calls if w["path"].endswith(".bak")
|
||||
]
|
||||
assert len(backup_calls) == 1
|
||||
assert backup_calls[0]["content"] == original
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_read_error(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
|
|||
|
|
@ -53,22 +53,24 @@ def _result_event(
|
|||
|
||||
|
||||
class TestGrepFieldMeta:
|
||||
def test_call_meta_contains_query_and_tool_name(self) -> None:
|
||||
def test_call_meta_contains_query_tool_name_and_search_path(self) -> None:
|
||||
event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src"))
|
||||
update = tool_call_session_update(event)
|
||||
|
||||
assert isinstance(update, ToolCallStart)
|
||||
assert update.field_meta == {"tool_name": "grep", "query": "TODO"}
|
||||
assert update.field_meta == {
|
||||
"tool_name": "grep",
|
||||
"query": "TODO",
|
||||
"search_path": str(Path("src").resolve()),
|
||||
}
|
||||
assert update.kind == "search"
|
||||
|
||||
def test_call_location_is_resolved_search_path(self) -> None:
|
||||
def test_call_has_no_locations_so_result_matches_are_not_replaced(self) -> None:
|
||||
event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src"))
|
||||
update = tool_call_session_update(event)
|
||||
|
||||
assert isinstance(update, ToolCallStart)
|
||||
assert update.locations is not None
|
||||
assert len(update.locations) == 1
|
||||
assert update.locations[0].path == str(Path("src").resolve())
|
||||
assert update.locations is None
|
||||
|
||||
def test_result_locations_from_parsed_matches(self) -> None:
|
||||
result = GrepResult(
|
||||
|
|
|
|||
|
|
@ -118,6 +118,61 @@ class TestAcpWriteFileExecution:
|
|||
assert params["path"] == str(test_file)
|
||||
assert params["content"] == "New content"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"])
|
||||
@pytest.mark.parametrize("os_newline", ["\n", "\r\n"])
|
||||
async def test_run_writes_with_os_linesep(
|
||||
self,
|
||||
input_newline: str,
|
||||
os_newline: str,
|
||||
mock_client: MockClient,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", os_newline)
|
||||
tool = WriteFile(
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test.txt"
|
||||
content = input_newline.join(["line 1", "line 2", "line 3"])
|
||||
args = WriteFileArgs(path=str(test_file), content=content)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert mock_client._last_write_params["content"] == os_newline.join([
|
||||
"line 1",
|
||||
"line 2",
|
||||
"line 3",
|
||||
])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_normalizes_mixed_newlines(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\n")
|
||||
tool = WriteFile(
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=mock_client, session_id="test_session"
|
||||
),
|
||||
)
|
||||
|
||||
test_file = tmp_path / "test.txt"
|
||||
args = WriteFileArgs(
|
||||
path=str(test_file), content="line 1\r\nline 2\nline 3\rline 4"
|
||||
)
|
||||
await collect_result(tool.run(args))
|
||||
|
||||
assert (
|
||||
mock_client._last_write_params["content"]
|
||||
== "line 1\nline 2\nline 3\nline 4"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_write_error(
|
||||
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
@ -209,6 +264,24 @@ class TestAcpWriteFileSessionUpdates:
|
|||
assert len(update.locations) == 1
|
||||
assert update.locations[0].path == str(Path("/tmp/test.txt").resolve())
|
||||
|
||||
def test_tool_call_session_update_passes_content_through(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\r\n")
|
||||
content = "line 1\r\nline 2\nline 3\rline 4"
|
||||
event = ToolCallEvent(
|
||||
tool_name="write_file",
|
||||
tool_call_id="test_call_123",
|
||||
args=WriteFileArgs(path="/tmp/test.txt", content=content),
|
||||
tool_class=WriteFile,
|
||||
)
|
||||
|
||||
update = WriteFile.tool_call_session_update(event)
|
||||
assert update is not None
|
||||
assert update.content is not None
|
||||
assert isinstance(update.content, list)
|
||||
assert update.content[0].new_text == content
|
||||
|
||||
def test_tool_call_session_update_invalid_args(self) -> None:
|
||||
from vibe.core.types import FunctionCall, ToolCall
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue