Protocol tests (16): - AgentRequest/AgentResponse serialization - DelegationIntent and DelegationReason validation - CoordinationResult aggregation - Error type tests Coordination tests (14): - Engine initialization and agent availability - Delegation execution (success, error, timeout) - Multi-intent coordination - Streaming delegation Librarian tests (42): - Library-desk client (all endpoints) - Wiki operations (search, get, create, update) - Smart-create with HybridRAG - Capability registration - Response model validation Total: 72 new tests, all passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
340 lines
11 KiB
Python
340 lines
11 KiB
Python
"""
|
|
Tests for multi-agent coordination engine.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from src.agents.coordination import (
|
|
CoordinationEngine,
|
|
get_coordination_engine,
|
|
delegate_to_librarian,
|
|
)
|
|
from src.agents.protocol import (
|
|
AgentResponse,
|
|
AgentUnavailableError,
|
|
DelegationIntent,
|
|
DelegationReason,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def coordination_engine():
|
|
"""Create a fresh coordination engine for testing."""
|
|
return CoordinationEngine()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_registry():
|
|
"""Mock the household registry."""
|
|
with patch("src.agents.coordination.get_household_registry") as mock:
|
|
registry = MagicMock()
|
|
mock.return_value = registry
|
|
yield registry
|
|
|
|
|
|
@pytest.fixture
|
|
def librarian_intent():
|
|
"""Create a standard librarian delegation intent."""
|
|
return DelegationIntent(
|
|
target_agent="librarian",
|
|
task="Find information about Docker networking",
|
|
reason=DelegationReason.DOMAIN_EXPERTISE,
|
|
expected_outcome="Documentation and examples",
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCoordinationEngine:
|
|
"""Tests for CoordinationEngine class."""
|
|
|
|
def test_initialization(self, coordination_engine):
|
|
"""Test engine initializes correctly."""
|
|
assert coordination_engine is not None
|
|
assert coordination_engine.registry is not None
|
|
|
|
def test_get_available_agents_empty(self, mock_registry):
|
|
"""Test getting available agents when none have agents."""
|
|
mock_registry.list_members.return_value = ["tatlock_core"]
|
|
mock_member = MagicMock()
|
|
mock_member.agent = None # No agent
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
engine = CoordinationEngine()
|
|
available = engine.get_available_agents()
|
|
|
|
assert available == []
|
|
|
|
def test_get_available_agents_with_librarian(self, mock_registry):
|
|
"""Test getting available agents with librarian registered."""
|
|
mock_registry.list_members.return_value = ["tatlock_core", "librarian"]
|
|
|
|
# tatlock_core has no agent
|
|
core_member = MagicMock()
|
|
core_member.agent = None
|
|
|
|
# librarian has an agent
|
|
librarian_member = MagicMock()
|
|
librarian_member.agent = MagicMock()
|
|
|
|
def get_member_side_effect(name):
|
|
if name == "tatlock_core":
|
|
return core_member
|
|
elif name == "librarian":
|
|
return librarian_member
|
|
return None
|
|
|
|
mock_registry.get_member.side_effect = get_member_side_effect
|
|
|
|
engine = CoordinationEngine()
|
|
available = engine.get_available_agents()
|
|
|
|
assert "librarian" in available
|
|
assert "tatlock_core" not in available
|
|
|
|
def test_can_delegate_to_unknown_agent(self, mock_registry):
|
|
"""Test checking delegation to unknown agent."""
|
|
mock_registry.get_member.return_value = None
|
|
|
|
engine = CoordinationEngine()
|
|
|
|
assert engine.can_delegate_to("unknown_agent") is False
|
|
|
|
def test_can_delegate_to_librarian(self, mock_registry):
|
|
"""Test checking delegation to librarian."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock() # Has an agent
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
engine = CoordinationEngine()
|
|
|
|
assert engine.can_delegate_to("librarian") is True
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDelegationExecution:
|
|
"""Tests for delegation execution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_delegation_unavailable_agent(
|
|
self, mock_registry, librarian_intent
|
|
):
|
|
"""Test delegation fails for unavailable agent."""
|
|
mock_registry.get_member.return_value = None
|
|
|
|
engine = CoordinationEngine()
|
|
|
|
with pytest.raises(AgentUnavailableError) as exc_info:
|
|
await engine.execute_delegation(librarian_intent)
|
|
|
|
assert "librarian" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_delegation_success(
|
|
self, mock_registry, librarian_intent
|
|
):
|
|
"""Test successful delegation execution."""
|
|
# Setup mock member with agent
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
# Mock the executor
|
|
with patch(
|
|
"src.agents.coordination.AGENT_EXECUTORS",
|
|
{"librarian": AsyncMock(return_value="Research results here")},
|
|
):
|
|
engine = CoordinationEngine()
|
|
response = await engine.execute_delegation(librarian_intent)
|
|
|
|
assert response.success is True
|
|
assert response.result == "Research results here"
|
|
# Duration might be 0 for very fast mock execution
|
|
assert response.duration_ms >= 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_delegation_error(
|
|
self, mock_registry, librarian_intent
|
|
):
|
|
"""Test delegation handles executor errors."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
# Mock executor that raises
|
|
async def failing_executor(**kwargs):
|
|
raise ValueError("API connection failed")
|
|
|
|
with patch(
|
|
"src.agents.coordination.AGENT_EXECUTORS",
|
|
{"librarian": failing_executor},
|
|
):
|
|
engine = CoordinationEngine()
|
|
response = await engine.execute_delegation(librarian_intent)
|
|
|
|
assert response.success is False
|
|
assert "API connection failed" in response.error_message
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestCoordinate:
|
|
"""Tests for multi-agent coordination."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_coordinate_single_intent(self, mock_registry, librarian_intent):
|
|
"""Test coordinating a single delegation."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
with patch(
|
|
"src.agents.coordination.AGENT_EXECUTORS",
|
|
{"librarian": AsyncMock(return_value="Found docs")},
|
|
):
|
|
engine = CoordinationEngine()
|
|
result = await engine.coordinate([librarian_intent])
|
|
|
|
assert result.final_response == "Found docs"
|
|
assert "librarian" in result.agents_consulted
|
|
# Duration might be 0 for very fast mock execution
|
|
assert result.total_duration_ms >= 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_coordinate_empty_intents(self, mock_registry):
|
|
"""Test coordinating with no intents."""
|
|
engine = CoordinationEngine()
|
|
result = await engine.coordinate([])
|
|
|
|
assert result.final_response == ""
|
|
assert result.agents_consulted == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_coordinate_multiple_intents(self, mock_registry):
|
|
"""Test coordinating multiple delegations."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
intents = [
|
|
DelegationIntent(
|
|
target_agent="librarian",
|
|
task="Task 1",
|
|
reason=DelegationReason.DOMAIN_EXPERTISE,
|
|
expected_outcome="Result 1",
|
|
priority=1,
|
|
),
|
|
DelegationIntent(
|
|
target_agent="librarian",
|
|
task="Task 2",
|
|
reason=DelegationReason.DOMAIN_EXPERTISE,
|
|
expected_outcome="Result 2",
|
|
priority=2,
|
|
),
|
|
]
|
|
|
|
call_count = 0
|
|
|
|
async def mock_executor(**kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return f"Result {call_count}"
|
|
|
|
with patch(
|
|
"src.agents.coordination.AGENT_EXECUTORS",
|
|
{"librarian": mock_executor},
|
|
):
|
|
engine = CoordinationEngine()
|
|
result = await engine.coordinate(intents)
|
|
|
|
# Both intents were executed (check agents_consulted count)
|
|
assert len(result.agents_consulted) == 2
|
|
# Current implementation replaces same-agent responses in dict
|
|
# So final_response has the last result (or combined if different agents)
|
|
assert len(result.final_response) > 0
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDelegateToLibrarian:
|
|
"""Tests for convenience delegation function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delegate_to_librarian(self, mock_registry):
|
|
"""Test the delegate_to_librarian helper."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
with patch(
|
|
"src.agents.coordination.AGENT_EXECUTORS",
|
|
{"librarian": AsyncMock(return_value="Wiki search results")},
|
|
):
|
|
# Reset global engine
|
|
with patch(
|
|
"src.agents.coordination._coordination_engine",
|
|
None,
|
|
):
|
|
response = await delegate_to_librarian(
|
|
task="Search for Docker docs",
|
|
context="Setting up homelab",
|
|
)
|
|
|
|
assert response.success is True
|
|
assert response.result == "Wiki search results"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestGetCoordinationEngine:
|
|
"""Tests for engine singleton."""
|
|
|
|
def test_get_coordination_engine_singleton(self):
|
|
"""Test engine is singleton."""
|
|
with patch("src.agents.coordination._coordination_engine", None):
|
|
engine1 = get_coordination_engine()
|
|
engine2 = get_coordination_engine()
|
|
|
|
# Should be same instance
|
|
assert engine1 is engine2
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestDelegationStreaming:
|
|
"""Tests for streaming delegation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_delegation_stream_unavailable(
|
|
self, mock_registry, librarian_intent
|
|
):
|
|
"""Test streaming fails for unavailable agent."""
|
|
engine = CoordinationEngine()
|
|
|
|
# Change target to an agent that doesn't have a stream executor
|
|
librarian_intent.target_agent = "nonexistent_agent"
|
|
|
|
with pytest.raises(AgentUnavailableError):
|
|
async for _ in engine.execute_delegation_stream(librarian_intent):
|
|
pass
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_delegation_stream_success(
|
|
self, mock_registry, librarian_intent
|
|
):
|
|
"""Test successful streaming delegation."""
|
|
mock_member = MagicMock()
|
|
mock_member.agent = MagicMock()
|
|
mock_registry.get_member.return_value = mock_member
|
|
|
|
async def mock_stream(**kwargs):
|
|
yield "Hello "
|
|
yield "world"
|
|
|
|
with patch(
|
|
"src.agents.coordination.AGENT_STREAM_EXECUTORS",
|
|
{"librarian": mock_stream},
|
|
):
|
|
engine = CoordinationEngine()
|
|
chunks = []
|
|
async for chunk in engine.execute_delegation_stream(librarian_intent):
|
|
chunks.append(chunk)
|
|
|
|
assert chunks == ["Hello ", "world"]
|