vibe/tests/tools/test_task.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

271 lines
11 KiB
Python

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from tests.conftest import build_test_vibe_config
from tests.mock.utils import collect_result
from vibe.core.agents.manager import AgentManager
from vibe.core.agents.models import BUILTIN_AGENTS, AgentType
from vibe.core.telemetry.types import TerminalEmulator
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError, ToolPermission
from vibe.core.tools.builtins.task import Task, TaskArgs, TaskResult, TaskToolConfig
from vibe.core.tools.permissions import PermissionContext
from vibe.core.types import AssistantEvent, LLMMessage, Role
@pytest.fixture
def task_tool() -> Task:
return Task(config_getter=lambda: TaskToolConfig(), state=BaseToolState())
class TestTaskArgs:
def test_default_agent_is_explore(self) -> None:
args = TaskArgs(task="do something")
assert args.agent == "explore"
def test_custom_values(self) -> None:
args = TaskArgs(task="do something", agent="explore")
assert args.task == "do something"
assert args.agent == "explore"
class TestTaskToolValidation:
@pytest.fixture
def ctx(self) -> InvokeContext:
config = build_test_vibe_config(
include_project_context=False, include_prompt_detail=False
)
manager = AgentManager(lambda: config)
return InvokeContext(
tool_call_id="test-call-id",
agent_manager=manager,
terminal_emulator=TerminalEmulator.VSCODE,
)
@pytest.mark.asyncio
async def test_rejects_primary_agent(
self, task_tool: Task, ctx: InvokeContext
) -> None:
args = TaskArgs(task="do something", agent="default")
with pytest.raises(ToolError) as exc_info:
await collect_result(task_tool.run(args, ctx))
assert "agent" in str(exc_info.value).lower()
assert "subagent" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_rejects_nonexistent_agent(
self, task_tool: Task, ctx: InvokeContext
) -> None:
args = TaskArgs(task="do something", agent="nonexistent")
with pytest.raises(ToolError) as exc_info:
await collect_result(task_tool.run(args, ctx))
assert "Unknown agent" in str(exc_info.value)
@pytest.mark.asyncio
async def test_requires_agent_manager_in_context(self, task_tool: Task) -> None:
args = TaskArgs(task="do something", agent="explore")
ctx = InvokeContext(tool_call_id="test-call-id") # No agent_manager
with pytest.raises(ToolError) as exc_info:
await collect_result(task_tool.run(args, ctx))
assert "agent_manager" in str(exc_info.value).lower()
def test_explore_agent_is_valid_subagent(self) -> None:
agent = BUILTIN_AGENTS["explore"]
assert agent.agent_type == AgentType.SUBAGENT
class TestTaskToolResolvePermission:
def test_explore_allowed_by_default(self, task_tool: Task) -> None:
args = TaskArgs(task="do something", agent="explore")
result = task_tool.resolve_permission(args)
assert isinstance(result, PermissionContext)
assert result.permission is ToolPermission.ALWAYS
def test_unknown_agent_returns_none(self, task_tool: Task) -> None:
args = TaskArgs(task="do something", agent="custom_agent")
result = task_tool.resolve_permission(args)
assert result is None
def test_denylist_takes_precedence(self) -> None:
config = TaskToolConfig(allowlist=["explore"], denylist=["explore"])
tool = Task(config_getter=lambda: config, state=BaseToolState())
args = TaskArgs(task="do something", agent="explore")
result = tool.resolve_permission(args)
assert isinstance(result, PermissionContext)
assert result.permission is ToolPermission.NEVER
def test_glob_pattern_in_allowlist(self) -> None:
config = TaskToolConfig(allowlist=["exp*"])
tool = Task(config_getter=lambda: config, state=BaseToolState())
args = TaskArgs(task="do something", agent="explore")
result = tool.resolve_permission(args)
assert isinstance(result, PermissionContext)
assert result.permission is ToolPermission.ALWAYS
def test_glob_pattern_in_denylist(self) -> None:
config = TaskToolConfig(denylist=["danger*"])
tool = Task(config_getter=lambda: config, state=BaseToolState())
args = TaskArgs(task="do something", agent="dangerous_agent")
result = tool.resolve_permission(args)
assert isinstance(result, PermissionContext)
assert result.permission is ToolPermission.NEVER
def test_empty_lists_returns_none(self) -> None:
config = TaskToolConfig(allowlist=[], denylist=[])
tool = Task(config_getter=lambda: config, state=BaseToolState())
args = TaskArgs(task="do something", agent="explore")
result = tool.resolve_permission(args)
assert result is None
def test_default_config_has_explore_in_allowlist(self) -> None:
config = TaskToolConfig()
assert "explore" in config.allowlist
class TestTaskToolExecution:
@pytest.fixture
def ctx(self) -> InvokeContext:
config = build_test_vibe_config(
include_project_context=False, include_prompt_detail=False
)
manager = AgentManager(lambda: config)
return InvokeContext(
tool_call_id="test-call-id",
agent_manager=manager,
terminal_emulator=TerminalEmulator.VSCODE,
)
@pytest.mark.asyncio
async def test_happy_path_returns_subagent_response(
self, task_tool: Task, ctx: InvokeContext
) -> None:
"""Test that task tool successfully runs a subagent and returns its response."""
mock_messages = [
LLMMessage(role=Role.system, content="system"),
LLMMessage(role=Role.user, content="task"),
LLMMessage(role=Role.assistant, content="response 1"),
LLMMessage(role=Role.assistant, content="response 2"),
]
async def mock_act(task: str):
yield AssistantEvent(content="Hello from subagent!")
yield AssistantEvent(content=" More content.")
with patch("vibe.core.tools.builtins.task.AgentLoop") as mock_agent_loop_class:
mock_agent_loop = MagicMock()
mock_agent_loop.act = mock_act
mock_agent_loop.messages = mock_messages
mock_agent_loop.set_approval_callback = MagicMock()
mock_agent_loop_class.return_value = mock_agent_loop
args = TaskArgs(task="explore the codebase", agent="explore")
result = await collect_result(task_tool.run(args, ctx))
assert isinstance(result, TaskResult)
assert result.response == "Hello from subagent! More content."
assert result.turns_used == 2 # 2 assistant messages in mock_messages
assert result.completed is True
assert (
mock_agent_loop_class.call_args.kwargs["terminal_emulator"]
is TerminalEmulator.VSCODE
)
@pytest.mark.asyncio
async def test_handles_stopped_by_middleware(
self, task_tool: Task, ctx: InvokeContext
) -> None:
"""Test that task tool reports incomplete when stopped by middleware."""
mock_messages = [
LLMMessage(role=Role.system, content="system"),
LLMMessage(role=Role.assistant, content="partial"),
]
async def mock_act(task: str):
yield AssistantEvent(content="Partial response", stopped_by_middleware=True)
with patch("vibe.core.tools.builtins.task.AgentLoop") as mock_agent_loop_class:
mock_agent_loop = MagicMock()
mock_agent_loop.act = mock_act
mock_agent_loop.messages = mock_messages
mock_agent_loop.set_approval_callback = MagicMock()
mock_agent_loop_class.return_value = mock_agent_loop
args = TaskArgs(task="do something", agent="explore")
result = await collect_result(task_tool.run(args, ctx))
assert isinstance(result, TaskResult)
assert result.completed is False
@pytest.mark.asyncio
async def test_handles_subagent_exception(
self, task_tool: Task, ctx: InvokeContext
) -> None:
"""Test that task tool gracefully handles exceptions from subagent."""
mock_messages = [LLMMessage(role=Role.system, content="system")]
async def mock_act(task: str):
yield AssistantEvent(content="Starting...")
raise RuntimeError("Simulated error")
with patch("vibe.core.tools.builtins.task.AgentLoop") as mock_agent_loop_class:
mock_agent_loop = MagicMock()
mock_agent_loop.act = mock_act
mock_agent_loop.messages = mock_messages
mock_agent_loop.set_approval_callback = MagicMock()
mock_agent_loop_class.return_value = mock_agent_loop
args = TaskArgs(task="do something", agent="explore")
result = await collect_result(task_tool.run(args, ctx))
assert isinstance(result, TaskResult)
assert result.completed is False
assert "Simulated error" in result.response
@pytest.mark.asyncio
async def test_closes_subagent_loop_on_success(
self, task_tool: Task, ctx: InvokeContext
) -> None:
async def mock_act(task: str):
yield AssistantEvent(content="done")
with patch("vibe.core.tools.builtins.task.AgentLoop") as mock_agent_loop_class:
mock_agent_loop = MagicMock()
mock_agent_loop.act = mock_act
mock_agent_loop.messages = [LLMMessage(role=Role.assistant, content="a")]
mock_agent_loop.set_approval_callback = MagicMock()
mock_agent_loop.aclose = AsyncMock()
mock_agent_loop_class.return_value = mock_agent_loop
args = TaskArgs(task="do something", agent="explore")
await collect_result(task_tool.run(args, ctx))
mock_agent_loop.aclose.assert_awaited_once()
@pytest.mark.asyncio
async def test_closes_subagent_loop_on_exception(
self, task_tool: Task, ctx: InvokeContext
) -> None:
async def mock_act(task: str):
yield AssistantEvent(content="starting")
raise RuntimeError("boom")
with patch("vibe.core.tools.builtins.task.AgentLoop") as mock_agent_loop_class:
mock_agent_loop = MagicMock()
mock_agent_loop.act = mock_act
mock_agent_loop.messages = [LLMMessage(role=Role.assistant, content="a")]
mock_agent_loop.set_approval_callback = MagicMock()
mock_agent_loop.aclose = AsyncMock()
mock_agent_loop_class.return_value = mock_agent_loop
args = TaskArgs(task="do something", agent="explore")
await collect_result(task_tool.run(args, ctx))
mock_agent_loop.aclose.assert_awaited_once()