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>
This commit is contained in:
Guillaume LE GOFF 2026-06-12 13:16:04 +02:00 committed by GitHub
parent 702d0f412e
commit cafb6d4147
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
247 changed files with 12401 additions and 3029 deletions

View file

@ -1,7 +1,9 @@
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
import json
from typing import cast
from pydantic import BaseModel
import pytest
@ -13,6 +15,7 @@ from tests.stubs.fake_tool import FakeTool
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import VibeConfig
from vibe.core.hooks.manager import HooksManager
from vibe.core.tools.base import ToolPermission
from vibe.core.tools.builtins.todo import TodoItem
from vibe.core.types import (
@ -480,6 +483,150 @@ async def test_tool_call_can_be_interrupted() -> None:
assert len(assistant_events) == 1
class _RecordingHooksManager:
def __init__(self) -> None:
self.invoked: list[str] = []
def reset_retry_count(self) -> None:
return
async def run(self, invocation: object) -> AsyncGenerator[object, None]:
self.invoked.append(str(getattr(invocation, "hook_event_name", "")))
return
yield
def _install_recording_hooks(agent_loop: AgentLoop) -> _RecordingHooksManager:
recorder = _RecordingHooksManager()
agent_loop._hooks_manager = cast(HooksManager, recorder)
return recorder
@pytest.mark.asyncio
async def test_after_tool_does_not_fire_when_cancel_lands_before_tool_execution() -> (
None
):
async def approval_callback(
_tool_name: str, _args: BaseModel, _tool_call_id: str, _rp: list | None = None
) -> tuple[ApprovalResponse, str | None]:
raise asyncio.CancelledError()
agent_loop = make_agent_loop(
auto_approve=False,
todo_permission=ToolPermission.ASK,
approval_callback=approval_callback,
backend=FakeBackend([
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_cancel_pre")],
)
],
[mock_llm_chunk(content="Cancelled.")],
]),
)
recorder = _install_recording_hooks(agent_loop)
events = await act_and_collect_events(agent_loop, "What's my todo list?")
tool_result = next((e for e in events if isinstance(e, ToolResultEvent)), None)
assert tool_result is not None
assert tool_result.cancelled is True
assert "before_tool" in recorder.invoked
assert "after_tool" not in recorder.invoked
@pytest.mark.asyncio
async def test_after_tool_does_not_fire_when_user_denies_at_approval_prompt() -> None:
async def approval_callback(
_tool_name: str, _args: BaseModel, _tool_call_id: str, _rp: list | None = None
) -> tuple[ApprovalResponse, str | None]:
return (ApprovalResponse.NO, "do not run this please")
agent_loop = make_agent_loop(
auto_approve=False,
todo_permission=ToolPermission.ASK,
approval_callback=approval_callback,
backend=FakeBackend([
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_user_deny")],
)
],
[mock_llm_chunk(content="OK, skipping.")],
]),
)
recorder = _install_recording_hooks(agent_loop)
events = await act_and_collect_events(agent_loop, "What's my todo list?")
tool_result = next((e for e in events if isinstance(e, ToolResultEvent)), None)
assert tool_result is not None
assert tool_result.skipped is True
assert tool_result.skip_reason == "do not run this please"
assert "before_tool" in recorder.invoked
assert "after_tool" not in recorder.invoked
@pytest.mark.asyncio
async def test_after_tool_does_not_fire_when_permission_is_never() -> None:
agent_loop = make_agent_loop(
auto_approve=False,
todo_permission=ToolPermission.NEVER,
backend=FakeBackend([
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_never")],
)
],
[mock_llm_chunk(content="OK.")],
]),
)
recorder = _install_recording_hooks(agent_loop)
events = await act_and_collect_events(agent_loop, "What's my todo list?")
tool_result = next((e for e in events if isinstance(e, ToolResultEvent)), None)
assert tool_result is not None
assert tool_result.skipped is True
assert "before_tool" in recorder.invoked
assert "after_tool" not in recorder.invoked
@pytest.mark.asyncio
async def test_after_tool_fires_when_cancel_lands_during_tool_execution() -> None:
tool_call = ToolCall(
id="call_cancel_mid",
index=0,
function=FunctionCall(name="stub_tool", arguments="{}"),
)
config = build_test_vibe_config(enabled_tools=["stub_tool"])
agent_loop = build_test_agent_loop(
config=config,
agent_name=BuiltinAgentName.AUTO_APPROVE,
backend=FakeBackend([
[mock_llm_chunk(content="Calling.", tool_calls=[tool_call])],
[mock_llm_chunk(content="Done.")],
]),
)
agent_loop.tool_manager._all_tools["stub_tool"] = FakeTool
stub_tool_instance = agent_loop.tool_manager.get("stub_tool")
assert isinstance(stub_tool_instance, FakeTool)
stub_tool_instance._exception_to_raise = asyncio.CancelledError()
recorder = _install_recording_hooks(agent_loop)
async for _ev in agent_loop.act("Execute tool"):
pass
assert "after_tool" in recorder.invoked
@pytest.mark.asyncio
async def test_fill_missing_tool_responses_inserts_placeholders() -> None:
agent_loop = build_test_agent_loop(