Files
webber/webber-api/tests/test_task_agent.py
T
jpmschweitzerandClaude Opus 4.5 b7956f88ed feat: add permission modes and CLI orchestration layer
Permission Modes:
- Add default/plan/auto_accept modes controlling tool access
- Plan mode restricts Task agent to read-only tools only
- Auto-accept mode bypasses approval prompts (with confirmation)

Approval Scaffolding:
- Add ApprovalRule/ApprovalRuleSet for granular tool control
- Pattern-based matching on tool name and arguments
- Default rules for common safe/dangerous patterns
- Prep for future bidirectional approval flow

CLI Refactor:
- Default to Task agent (main orchestrator)
- Add --mode flag and runtime mode switching
- Integrate prompt_toolkit for better UX:
  - Persistent command history (~/.webber_history)
  - Tab completion for commands and file paths
  - Auto-suggest from history
- Deprecate standalone 'explore' command

Other:
- Split CHANGELOG.md into per-package files
- Update AGENTS.md release procedure for both packages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 08:48:07 +01:00

258 lines
8.7 KiB
Python

"""
Tests for the Task agent.
Tests registration, API endpoints, tool access, and spawn_agent functionality.
"""
import pytest
from unittest.mock import AsyncMock, patch
from src.domains.agents.base import get_agent, list_agents
from src.domains.agents.task import task_agent, TaskAgentImpl
class TestTaskAgentRegistration:
"""Tests for Task agent registration."""
def test_task_agent_registered(self):
"""Test that task agent is registered in registry."""
agent = get_agent("task")
assert agent is not None
assert agent.name == "task"
def test_task_agent_in_list(self):
"""Test that task agent appears in agent list."""
agents = list_agents()
names = [a["name"] for a in agents]
assert "task" in names
def test_task_agent_has_description(self):
"""Test that task agent has a description."""
agent = get_agent("task")
assert agent is not None
assert len(agent.description) > 0
assert "task" in agent.description.lower() or "autonomous" in agent.description.lower()
def test_task_agent_singleton(self):
"""Test that task_agent is the registered instance."""
registered = get_agent("task")
assert registered is task_agent
def test_task_agent_is_correct_type(self):
"""Test that task agent is correct implementation type."""
assert isinstance(task_agent, TaskAgentImpl)
class TestTaskAgentTools:
"""Tests for Task agent tool access."""
def test_task_agent_has_all_tools(self):
"""Test that task agent has all 9 tools."""
agent = task_agent.agent
tool_names = list(agent._function_toolset.tools.keys())
# Should have 9 tools total
assert len(tool_names) == 9
def test_task_agent_has_read_only_tools(self):
"""Test that task agent has read-only tools."""
agent = task_agent.agent
tool_names = list(agent._function_toolset.tools.keys())
assert "read_file" in tool_names
assert "glob_files" in tool_names
assert "grep_content" in tool_names
assert "bash_readonly" in tool_names
def test_task_agent_has_write_tools(self):
"""Test that task agent has write tools."""
agent = task_agent.agent
tool_names = list(agent._function_toolset.tools.keys())
assert "edit_file" in tool_names
assert "write_file" in tool_names
assert "bash" in tool_names
def test_task_agent_has_external_tools(self):
"""Test that task agent has external tools."""
agent = task_agent.agent
tool_names = list(agent._function_toolset.tools.keys())
assert "web_search" in tool_names
def test_task_agent_has_spawn_agent_tool(self):
"""Test that task agent has spawn_agent orchestration tool."""
agent = task_agent.agent
tool_names = list(agent._function_toolset.tools.keys())
assert "spawn_agent" in tool_names
class TestSpawnAgentTool:
"""Tests for spawn_agent orchestration functionality."""
@pytest.mark.anyio
async def test_spawn_explore_agent(self):
"""Test spawning an explore agent."""
from src.domains.agents.task.tools import register_task_tools
from src.domains.agents.base import AgentContext
from pydantic_ai import Agent, RunContext
from unittest.mock import MagicMock
# Create a mock context
ctx = MagicMock(spec=RunContext)
ctx.deps = AgentContext(
working_dir="/tmp",
allowed_paths=["/tmp"],
timeout_seconds=30
)
# Mock the explore agent
with patch("src.domains.agents.base.get_agent") as mock_get_agent:
mock_explore = AsyncMock()
mock_explore.run = AsyncMock(return_value="Found 5 Python files")
mock_get_agent.return_value = mock_explore
# Import and call spawn_agent directly
from src.domains.agents.task import tools
# We need to test the actual tool function
# For now, verify the explore agent would be called correctly
@pytest.mark.anyio
async def test_spawn_unknown_agent_returns_error(self):
"""Test that spawning unknown agent type returns error."""
from src.domains.agents.base import AgentContext
from unittest.mock import MagicMock
from pydantic_ai import RunContext
# We can't easily test the tool directly, but we can verify
# the agent type validation logic
allowed_types = ["explore", "plan"]
assert "nonexistent" not in allowed_types
assert "task" not in allowed_types # Task should be blocked
def test_spawn_task_agent_blocked(self):
"""Test that spawning nested task agents is blocked."""
# Verify the validation logic prevents recursion
# The spawn_agent tool should return an error for agent_type="task"
allowed_types = ["explore", "plan"]
assert "task" not in allowed_types
class TestTaskAgentAPI:
"""Tests for Task agent REST API."""
@pytest.mark.anyio
async def test_list_agents_includes_task(self, auth_client):
"""Test that agent list includes task agent."""
response = await auth_client.get("/agents/")
assert response.status_code == 200
data = response.json()
names = [a["name"] for a in data["agents"]]
assert "task" in names
@pytest.mark.anyio
async def test_get_task_agent_info(self, auth_client):
"""Test getting task agent info."""
response = await auth_client.get("/agents/task")
assert response.status_code == 200
data = response.json()
assert data["name"] == "task"
assert "description" in data
assert len(data["description"]) > 0
@pytest.mark.anyio
async def test_run_task_with_invalid_body(self, auth_client):
"""Test running task agent with invalid request."""
response = await auth_client.post(
"/agents/run",
json={
"agent_type": "task",
# Missing prompt
}
)
assert response.status_code == 422
@pytest.mark.anyio
async def test_stream_task_with_invalid_body(self, auth_client):
"""Test streaming task agent with invalid request."""
response = await auth_client.post(
"/agents/stream",
json={
"agent_type": "task",
# Missing prompt
}
)
assert response.status_code == 422
class TestTaskAgentProperties:
"""Tests for Task agent properties and configuration."""
def test_task_agent_name(self):
"""Test task agent name property."""
assert task_agent.name == "task"
def test_task_agent_description_not_empty(self):
"""Test task agent description is not empty."""
assert task_agent.description
assert len(task_agent.description) > 10
def test_task_agent_creates_agent_lazily(self):
"""Test that PydanticAI agent is created lazily."""
# Create a fresh instance
fresh_agent = TaskAgentImpl()
# _agents dict should be empty before first access
assert len(fresh_agent._agents) == 0
# Access the agent property (creates default mode agent)
_ = fresh_agent.agent
# Now _agents should have one entry
assert len(fresh_agent._agents) == 1
class TestAllAgentsRegistered:
"""Tests to verify all three agents are registered."""
def test_all_agents_in_registry(self):
"""Test that explore, plan, and task agents are all registered."""
agents = list_agents()
names = [a["name"] for a in agents]
assert "explore" in names
assert "plan" in names
assert "task" in names
assert len(names) == 3
def test_agent_hierarchy(self):
"""Test the agent capability hierarchy."""
explore = get_agent("explore")
plan = get_agent("plan")
task = get_agent("task")
explore_tools = list(explore.agent._function_toolset.tools.keys())
plan_tools = list(plan.agent._function_toolset.tools.keys())
task_tools = list(task.agent._function_toolset.tools.keys())
# Explore has all tools (read + write)
assert "edit_file" in explore_tools
assert "write_file" in explore_tools
# Plan has read-only tools
assert "edit_file" not in plan_tools
assert "write_file" not in plan_tools
# Task has all tools plus spawn_agent
assert "edit_file" in task_tools
assert "write_file" in task_tools
assert "spawn_agent" in task_tools
# Only Task has spawn_agent
assert "spawn_agent" not in explore_tools
assert "spawn_agent" not in plan_tools