from __future__ import annotations from unittest.mock import 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.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) @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) @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 @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