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>
This commit is contained in:
parent
564a14365e
commit
6bedf271ce
223 changed files with 10533 additions and 6947 deletions
170
tests/agent_loop/e2e/test_e2e_bash.py
Normal file
170
tests/agent_loop/e2e/test_e2e_bash.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
import pytest
|
||||
|
||||
from tests.agent_loop.e2e.conftest import MistralAPI, build_e2e_agent_loop, e2e_config
|
||||
from tests.backend.data.mistral import mistral_completion
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.tools.builtins.bash import BashResult
|
||||
from vibe.core.tools.permissions import RequiredPermission
|
||||
from vibe.core.types import ApprovalResponse, BaseEvent, ToolResultEvent
|
||||
|
||||
|
||||
def _bash_call(command: str, timeout: int | None = None) -> dict[str, Any]:
|
||||
arguments: dict[str, Any] = {"command": command}
|
||||
if timeout is not None:
|
||||
arguments["timeout"] = timeout
|
||||
return mistral_completion(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_bash",
|
||||
"function": {"name": "bash", "arguments": json.dumps(arguments)},
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def _run_bash(
|
||||
mistral_api: MistralAPI,
|
||||
command: str,
|
||||
*,
|
||||
timeout: int | None = None,
|
||||
agent_name: str = BuiltinAgentName.AUTO_APPROVE,
|
||||
approval: ApprovalResponse | None = None,
|
||||
) -> ToolResultEvent:
|
||||
mistral_api.reply(_bash_call(command, timeout), mistral_completion("done"))
|
||||
agent = build_e2e_agent_loop(
|
||||
config=e2e_config(enabled_tools=["bash"]), agent_name=agent_name
|
||||
)
|
||||
if approval is not None:
|
||||
|
||||
async def approval_callback(
|
||||
_tool_name: str,
|
||||
_args: BaseModel,
|
||||
_tool_call_id: str,
|
||||
_rp: list[RequiredPermission] | None = None,
|
||||
) -> tuple[ApprovalResponse, str | None]:
|
||||
return (approval, None)
|
||||
|
||||
agent.set_approval_callback(approval_callback)
|
||||
|
||||
events: list[BaseEvent] = [event async for event in agent.act("go")]
|
||||
return next(e for e in events if isinstance(e, ToolResultEvent))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_captures_stdout(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(mistral_api, "echo hello")
|
||||
|
||||
bash_result = cast(BashResult, result.result)
|
||||
assert bash_result.returncode == 0
|
||||
assert "hello" in bash_result.stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_captures_stderr(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(mistral_api, "echo oops >&2")
|
||||
|
||||
bash_result = cast(BashResult, result.result)
|
||||
assert "oops" in bash_result.stderr
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_nonzero_exit_surfaces_as_error(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(mistral_api, "exit 3")
|
||||
|
||||
assert result.error is not None
|
||||
assert "Return code: 3" in result.error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_timeout_surfaces_as_error(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(mistral_api, "sleep 5", timeout=1)
|
||||
|
||||
assert result.error is not None
|
||||
assert "timed out" in result.error.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_output_truncated_to_max_bytes(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(mistral_api, "yes x | head -c 100000")
|
||||
|
||||
bash_result = cast(BashResult, result.result)
|
||||
assert len(bash_result.stdout) <= 16_000
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_denylisted_command_is_skipped(mistral_api: MistralAPI) -> None:
|
||||
result = await _run_bash(
|
||||
mistral_api, "vim file.txt", agent_name=BuiltinAgentName.DEFAULT
|
||||
)
|
||||
|
||||
assert result.skipped is True
|
||||
assert result.skip_reason is not None
|
||||
assert "denied" in result.skip_reason.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_allowlisted_command_runs_without_approval(
|
||||
mistral_api: MistralAPI,
|
||||
) -> None:
|
||||
# No approval callback registered; an allowlisted command must run anyway.
|
||||
result = await _run_bash(
|
||||
mistral_api, "echo allowed", agent_name=BuiltinAgentName.DEFAULT
|
||||
)
|
||||
|
||||
assert result.skipped is False
|
||||
assert "allowed" in cast(BashResult, result.result).stdout
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_non_allowlisted_command_requires_approval(
|
||||
mistral_api: MistralAPI,
|
||||
) -> None:
|
||||
result = await _run_bash(
|
||||
mistral_api,
|
||||
"touch newfile.txt",
|
||||
agent_name=BuiltinAgentName.DEFAULT,
|
||||
approval=ApprovalResponse.YES,
|
||||
)
|
||||
|
||||
assert result.skipped is False
|
||||
assert (Path.cwd() / "newfile.txt").exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_non_allowlisted_command_denied_at_prompt_is_skipped(
|
||||
mistral_api: MistralAPI,
|
||||
) -> None:
|
||||
result = await _run_bash(
|
||||
mistral_api,
|
||||
"touch denied.txt",
|
||||
agent_name=BuiltinAgentName.DEFAULT,
|
||||
approval=ApprovalResponse.NO,
|
||||
)
|
||||
|
||||
assert result.skipped is True
|
||||
assert not (Path.cwd() / "denied.txt").exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bash_command_touching_outside_workdir_requires_approval(
|
||||
mistral_api: MistralAPI, tmp_path: Path
|
||||
) -> None:
|
||||
outside = tmp_path / "outside.txt"
|
||||
result = await _run_bash(
|
||||
mistral_api,
|
||||
f"touch {outside}",
|
||||
agent_name=BuiltinAgentName.DEFAULT,
|
||||
approval=ApprovalResponse.NO,
|
||||
)
|
||||
|
||||
assert result.skipped is True
|
||||
assert not outside.exists()
|
||||
Loading…
Add table
Add a link
Reference in a new issue