Files
Timmy-time-dashboard/tests/test_tool_executor.py
Alexander Payne 14072f9bb5 feat: MCP tools integration for swarm agents
ToolExecutor:
- Persona-specific toolkit selection (forge gets code tools, echo gets search)
- Tool inference from task keywords (search→web_search, code→python)
- LLM-powered reasoning about tool selection
- Graceful degradation when Agno unavailable

PersonaNode Updates:
- Subscribe to swarm:events for task assignments
- Execute tasks using ToolExecutor when assigned
- Complete tasks via comms.complete_task()
- Track current_task for status monitoring

Tests:
- 19 new tests for tool execution
- All 6 personas covered
- Tool inference verification
- Edge cases (no toolkit, unknown tasks)

Total: 491 tests passing
2026-02-22 20:33:26 -05:00

212 lines
8.0 KiB
Python

"""Tests for MCP tool execution in swarm agents.
Covers:
- ToolExecutor initialization for each persona
- Task execution with appropriate tools
- Tool inference from task descriptions
- Error handling when tools unavailable
Note: These tests run with mocked Agno, so actual tool availability
may be limited. Tests verify the interface works correctly.
"""
import pytest
from pathlib import Path
from swarm.tool_executor import ToolExecutor
from swarm.persona_node import PersonaNode
from swarm.comms import SwarmComms
class TestToolExecutor:
"""Tests for the ToolExecutor class."""
def test_create_for_persona_forge(self):
"""Can create executor for Forge (coding) persona."""
executor = ToolExecutor.for_persona("forge", "forge-test-001")
assert executor._persona_id == "forge"
assert executor._agent_id == "forge-test-001"
def test_create_for_persona_echo(self):
"""Can create executor for Echo (research) persona."""
executor = ToolExecutor.for_persona("echo", "echo-test-001")
assert executor._persona_id == "echo"
assert executor._agent_id == "echo-test-001"
def test_get_capabilities_returns_list(self):
"""get_capabilities returns list (may be empty if tools unavailable)."""
executor = ToolExecutor.for_persona("forge", "forge-test-001")
caps = executor.get_capabilities()
assert isinstance(caps, list)
# Note: In tests with mocked Agno, this may be empty
def test_describe_tools_returns_string(self):
"""Tool descriptions are generated as string."""
executor = ToolExecutor.for_persona("forge", "forge-test-001")
desc = executor._describe_tools()
assert isinstance(desc, str)
# When toolkit is None, returns "No tools available"
def test_infer_tools_for_code_task(self):
"""Correctly infers tools needed for coding tasks."""
executor = ToolExecutor.for_persona("forge", "forge-test-001")
task = "Write a Python function to calculate fibonacci"
tools = executor._infer_tools_needed(task)
# Should infer python tool from keywords
assert "python" in tools
def test_infer_tools_for_search_task(self):
"""Correctly infers tools needed for research tasks."""
executor = ToolExecutor.for_persona("echo", "echo-test-001")
task = "Search for information about Python asyncio"
tools = executor._infer_tools_needed(task)
# Should infer web_search from "search" keyword
assert "web_search" in tools
def test_infer_tools_for_file_task(self):
"""Correctly infers tools needed for file operations."""
executor = ToolExecutor.for_persona("quill", "quill-test-001")
task = "Read the README file and write a summary"
tools = executor._infer_tools_needed(task)
# Should infer read_file from "read" keyword
assert "read_file" in tools
def test_execute_task_returns_dict(self):
"""Task execution returns result dict."""
executor = ToolExecutor.for_persona("echo", "echo-test-001")
result = executor.execute_task("What is the weather today?")
assert isinstance(result, dict)
assert "success" in result
assert "result" in result
assert "tools_used" in result
def test_execute_task_includes_metadata(self):
"""Task result includes persona and agent IDs."""
executor = ToolExecutor.for_persona("seer", "seer-test-001")
result = executor.execute_task("Analyze this data")
# Check metadata is present when execution succeeds
if result.get("success"):
assert result.get("persona_id") == "seer"
assert result.get("agent_id") == "seer-test-001"
def test_execute_task_handles_empty_toolkit(self):
"""Execution handles case where toolkit is None."""
executor = ToolExecutor("unknown", "unknown-001")
executor._toolkit = None # Force None
result = executor.execute_task("Some task")
# Should still return a result even without toolkit
assert isinstance(result, dict)
assert "success" in result or "result" in result
class TestPersonaNodeToolIntegration:
"""Tests for PersonaNode integration with tools."""
def test_persona_node_has_tool_executor(self):
"""PersonaNode initializes with tool executor (or None if tools unavailable)."""
comms = SwarmComms()
node = PersonaNode("forge", "forge-test-001", comms=comms)
# Should have tool executor attribute
assert hasattr(node, '_tool_executor')
def test_persona_node_tool_capabilities(self):
"""PersonaNode exposes tool capabilities (may be empty in tests)."""
comms = SwarmComms()
node = PersonaNode("forge", "forge-test-001", comms=comms)
caps = node.tool_capabilities
assert isinstance(caps, list)
# Note: May be empty in tests with mocked Agno
def test_persona_node_tracks_current_task(self):
"""PersonaNode tracks currently executing task."""
comms = SwarmComms()
node = PersonaNode("echo", "echo-test-001", comms=comms)
# Initially no current task
assert node.current_task is None
def test_persona_node_handles_unknown_task(self):
"""PersonaNode handles task not found gracefully."""
comms = SwarmComms()
node = PersonaNode("forge", "forge-test-001", comms=comms)
# Try to handle non-existent task
# This should log error but not crash
node._handle_task_assignment("non-existent-task-id")
# Should have no current task after handling
assert node.current_task is None
class TestToolInference:
"""Tests for tool inference from task descriptions."""
def test_infer_shell_from_command_keyword(self):
"""Shell tool inferred from 'command' keyword."""
executor = ToolExecutor.for_persona("helm", "helm-test")
tools = executor._infer_tools_needed("Run the deploy command")
assert "shell" in tools
def test_infer_write_file_from_save_keyword(self):
"""Write file tool inferred from 'save' keyword."""
executor = ToolExecutor.for_persona("quill", "quill-test")
tools = executor._infer_tools_needed("Save this to a file")
assert "write_file" in tools
def test_infer_list_files_from_directory_keyword(self):
"""List files tool inferred from 'directory' keyword."""
executor = ToolExecutor.for_persona("echo", "echo-test")
tools = executor._infer_tools_needed("List files in the directory")
assert "list_files" in tools
def test_no_duplicate_tools(self):
"""Tool inference doesn't duplicate tools."""
executor = ToolExecutor.for_persona("forge", "forge-test")
# Task with multiple code keywords
tools = executor._infer_tools_needed("Code a python script")
# Should only have python once
assert tools.count("python") == 1
class TestToolExecutionIntegration:
"""Integration tests for tool execution flow."""
def test_task_execution_with_tools_unavailable(self):
"""Task execution works even when Agno tools unavailable."""
executor = ToolExecutor.for_persona("echo", "echo-no-tools")
# Force toolkit to None to simulate unavailable tools
executor._toolkit = None
executor._llm = None
result = executor.execute_task("Search for something")
# Should still return a valid result
assert isinstance(result, dict)
assert "result" in result
# Tools should still be inferred even if not available
assert "tools_used" in result