test(phase3): add comprehensive tests for multi-agent coordination
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>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for The Librarian agent."""
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Tests for Librarian capability registration.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.agents.librarian.capability import (
|
||||
LIBRARIAN_CAPABILITY,
|
||||
get_librarian_capability,
|
||||
register_librarian,
|
||||
unregister_librarian,
|
||||
)
|
||||
from src.core.household_registry import HouseholdCapability
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLibrarianCapability:
|
||||
"""Tests for the Librarian capability definition."""
|
||||
|
||||
def test_capability_is_household_capability(self):
|
||||
"""Test capability is correct type."""
|
||||
assert isinstance(LIBRARIAN_CAPABILITY, HouseholdCapability)
|
||||
|
||||
def test_capability_name(self):
|
||||
"""Test capability has correct name."""
|
||||
assert LIBRARIAN_CAPABILITY.name == "librarian"
|
||||
|
||||
def test_capability_role(self):
|
||||
"""Test capability has correct role."""
|
||||
assert LIBRARIAN_CAPABILITY.role == "The Librarian"
|
||||
|
||||
def test_capability_category(self):
|
||||
"""Test capability is in research category."""
|
||||
assert LIBRARIAN_CAPABILITY.category == "research"
|
||||
|
||||
def test_capability_domains(self):
|
||||
"""Test capability covers expected domains."""
|
||||
domains = LIBRARIAN_CAPABILITY.domains
|
||||
|
||||
assert "research" in domains
|
||||
assert "knowledge" in domains
|
||||
assert "wiki" in domains
|
||||
assert "search" in domains
|
||||
|
||||
def test_capability_requires_network(self):
|
||||
"""Test capability requires network access."""
|
||||
assert LIBRARIAN_CAPABILITY.requires_network is True
|
||||
|
||||
def test_get_librarian_capability(self):
|
||||
"""Test getter returns same capability."""
|
||||
cap = get_librarian_capability()
|
||||
|
||||
assert cap is LIBRARIAN_CAPABILITY
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLibrarianRegistration:
|
||||
"""Tests for Librarian registration functions."""
|
||||
|
||||
def test_register_librarian(self):
|
||||
"""Test registering librarian with registry."""
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.__contains__ = MagicMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.capability.get_household_registry",
|
||||
return_value=mock_registry,
|
||||
):
|
||||
with patch(
|
||||
"src.agents.librarian.capability.get_librarian_agent"
|
||||
) as mock_get_agent:
|
||||
mock_agent = MagicMock()
|
||||
mock_get_agent.return_value = mock_agent
|
||||
|
||||
register_librarian()
|
||||
|
||||
mock_registry.register.assert_called_once()
|
||||
call_kwargs = mock_registry.register.call_args[1]
|
||||
|
||||
assert call_kwargs["name"] == "librarian"
|
||||
assert call_kwargs["capability"] is LIBRARIAN_CAPABILITY
|
||||
assert call_kwargs["agent"] is mock_agent
|
||||
|
||||
def test_register_librarian_already_registered(self):
|
||||
"""Test registering when already registered does nothing."""
|
||||
mock_registry = MagicMock()
|
||||
mock_registry.__contains__ = MagicMock(return_value=True)
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.capability.get_household_registry",
|
||||
return_value=mock_registry,
|
||||
):
|
||||
register_librarian()
|
||||
|
||||
# Should not call register since already registered
|
||||
mock_registry.register.assert_not_called()
|
||||
|
||||
def test_unregister_librarian(self):
|
||||
"""Test unregistering librarian from registry."""
|
||||
mock_registry = MagicMock()
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.capability.get_household_registry",
|
||||
return_value=mock_registry,
|
||||
):
|
||||
unregister_librarian()
|
||||
|
||||
mock_registry.unregister.assert_called_once_with("librarian")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCapabilityDescription:
|
||||
"""Tests for capability description."""
|
||||
|
||||
def test_description_mentions_library_desk(self):
|
||||
"""Test description mentions library-desk API."""
|
||||
assert "library-desk" in LIBRARIAN_CAPABILITY.description.lower()
|
||||
|
||||
def test_description_mentions_search(self):
|
||||
"""Test description mentions search capability."""
|
||||
assert "search" in LIBRARIAN_CAPABILITY.description.lower()
|
||||
|
||||
def test_description_mentions_wiki(self):
|
||||
"""Test description mentions wiki access."""
|
||||
assert "wiki" in LIBRARIAN_CAPABILITY.description.lower()
|
||||
@@ -0,0 +1,598 @@
|
||||
"""
|
||||
Tests for the Library-Desk HTTP client.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import httpx
|
||||
|
||||
from src.agents.librarian.client import (
|
||||
LibraryDeskClient,
|
||||
HybridRAGResponse,
|
||||
HybridSearchResult,
|
||||
WikiPage,
|
||||
WikiSearchResult,
|
||||
VectorSearchResult,
|
||||
GraphNode,
|
||||
Dossier,
|
||||
SmartCreateResponse,
|
||||
ResearchSummary,
|
||||
EntityLinking,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_client():
|
||||
"""Create a mock httpx client."""
|
||||
return AsyncMock(spec=httpx.AsyncClient)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_with_mock(mock_httpx_client):
|
||||
"""Create a LibraryDeskClient with mocked httpx client."""
|
||||
client = LibraryDeskClient(
|
||||
base_url="http://test:8089",
|
||||
api_key="test-key",
|
||||
)
|
||||
client._client = mock_httpx_client
|
||||
return client
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLibraryDeskClientInit:
|
||||
"""Tests for client initialization."""
|
||||
|
||||
def test_default_initialization(self):
|
||||
"""Test client initializes with defaults from config."""
|
||||
client = LibraryDeskClient()
|
||||
|
||||
assert client.base_url is not None
|
||||
assert client.timeout == 60
|
||||
assert client._client is None
|
||||
|
||||
def test_custom_initialization(self):
|
||||
"""Test client with custom parameters."""
|
||||
client = LibraryDeskClient(
|
||||
base_url="http://custom:9000",
|
||||
api_key="my-api-key",
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
assert client.base_url == "http://custom:9000"
|
||||
assert client.api_key == "my-api-key"
|
||||
assert client.timeout == 120
|
||||
|
||||
def test_ensure_client_not_initialized(self):
|
||||
"""Test _ensure_client raises when not in context."""
|
||||
client = LibraryDeskClient()
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
client._ensure_client()
|
||||
|
||||
assert "not initialized" in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestContextManager:
|
||||
"""Tests for async context manager."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_manager_creates_client(self):
|
||||
"""Test context manager creates httpx client."""
|
||||
async with LibraryDeskClient(
|
||||
base_url="http://test:8089",
|
||||
api_key="test-key",
|
||||
) as client:
|
||||
assert client._client is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_manager_closes_client(self):
|
||||
"""Test context manager closes client on exit."""
|
||||
client = LibraryDeskClient(base_url="http://test:8089")
|
||||
|
||||
async with client:
|
||||
assert client._client is not None
|
||||
|
||||
# After exit, client should be None
|
||||
assert client._client is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHybridSearch:
|
||||
"""Tests for hybrid search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hybrid_search_success(self, client_with_mock, mock_httpx_client):
|
||||
"""Test successful hybrid search."""
|
||||
# Mock response
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"results": [
|
||||
{
|
||||
"source": "vector",
|
||||
"title": "Docker Guide",
|
||||
"content": "Docker networking basics...",
|
||||
"score": 0.95,
|
||||
"page_id": 123,
|
||||
}
|
||||
],
|
||||
"keywords": ["docker", "networking"],
|
||||
"synonyms": ["container"],
|
||||
"formatted_context": "Context here",
|
||||
"timing": {"total": 1.5},
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
result = await client_with_mock.hybrid_search(
|
||||
query="Docker networking",
|
||||
user="testuser",
|
||||
)
|
||||
|
||||
assert isinstance(result, HybridRAGResponse)
|
||||
assert len(result.results) == 1
|
||||
assert result.results[0].title == "Docker Guide"
|
||||
assert result.results[0].source == "vector"
|
||||
assert "docker" in result.keywords
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hybrid_search_empty_results(
|
||||
self, client_with_mock, mock_httpx_client
|
||||
):
|
||||
"""Test hybrid search with no results."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"results": [],
|
||||
"keywords": [],
|
||||
"formatted_context": "",
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
result = await client_with_mock.hybrid_search("nonexistent query")
|
||||
|
||||
assert len(result.results) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWikiOperations:
|
||||
"""Tests for wiki operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_wiki(self, client_with_mock, mock_httpx_client):
|
||||
"""Test wiki search."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"path": "/docs/docker",
|
||||
"title": "Docker Documentation",
|
||||
"description": "Docker docs",
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
results = await client_with_mock.search_wiki("docker")
|
||||
|
||||
assert len(results) == 1
|
||||
assert isinstance(results[0], WikiSearchResult)
|
||||
assert results[0].title == "Docker Documentation"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wiki_page(self, client_with_mock, mock_httpx_client):
|
||||
"""Test getting a wiki page."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"id": 123,
|
||||
"path": "/docs/docker",
|
||||
"title": "Docker Guide",
|
||||
"content": "# Docker\n\nFull content here...",
|
||||
"tags": ["docker", "devops"],
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
page = await client_with_mock.get_wiki_page(123)
|
||||
|
||||
assert isinstance(page, WikiPage)
|
||||
assert page.id == 123
|
||||
assert page.title == "Docker Guide"
|
||||
assert "docker" in page.tags
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_wiki_pages(self, client_with_mock, mock_httpx_client):
|
||||
"""Test listing wiki pages."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"pages": [
|
||||
{"id": 1, "path": "/page1", "title": "Page 1"},
|
||||
{"id": 2, "path": "/page2", "title": "Page 2"},
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
pages = await client_with_mock.list_wiki_pages()
|
||||
|
||||
assert len(pages) == 2
|
||||
assert pages[0].title == "Page 1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_dossiers(self, client_with_mock, mock_httpx_client):
|
||||
"""Test listing dossiers."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"dossiers": [
|
||||
{"name": "docker", "page_count": 10},
|
||||
{"name": "kubernetes", "page_count": 5},
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
dossiers = await client_with_mock.list_dossiers()
|
||||
|
||||
assert len(dossiers) == 2
|
||||
assert isinstance(dossiers[0], Dossier)
|
||||
assert dossiers[0].name == "docker"
|
||||
assert dossiers[0].page_count == 10
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSemanticSearch:
|
||||
"""Tests for semantic/vector search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_semantic_search(self, client_with_mock, mock_httpx_client):
|
||||
"""Test semantic search."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"results": [
|
||||
{
|
||||
"page_id": 1,
|
||||
"page_path": "/docs/networking",
|
||||
"page_title": "Networking Guide",
|
||||
"chunk_text": "Container networking...",
|
||||
"score": 0.92,
|
||||
"chunk_index": 0,
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
results = await client_with_mock.semantic_search("container networking")
|
||||
|
||||
assert len(results) == 1
|
||||
assert isinstance(results[0], VectorSearchResult)
|
||||
assert results[0].score == 0.92
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGraphOperations:
|
||||
"""Tests for knowledge graph operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_graph(self, client_with_mock, mock_httpx_client):
|
||||
"""Test executing a Cypher query."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"records": [
|
||||
{"name": "Docker", "type": "Technology"},
|
||||
{"name": "Kubernetes", "type": "Technology"},
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
records = await client_with_mock.query_graph(
|
||||
"MATCH (n:Technology) RETURN n.name as name, n.type as type"
|
||||
)
|
||||
|
||||
assert len(records) == 2
|
||||
assert records[0]["name"] == "Docker"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_graph_nodes(self, client_with_mock, mock_httpx_client):
|
||||
"""Test listing graph nodes."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node1",
|
||||
"labels": ["Technology"],
|
||||
"properties": {"name": "Docker"},
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
nodes = await client_with_mock.list_graph_nodes()
|
||||
|
||||
assert len(nodes) == 1
|
||||
assert isinstance(nodes[0], GraphNode)
|
||||
assert nodes[0].id == "node1"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHealthCheck:
|
||||
"""Tests for health check."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_healthy(self, client_with_mock, mock_httpx_client):
|
||||
"""Test health check returns true when healthy."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_httpx_client.get.return_value = mock_response
|
||||
|
||||
result = await client_with_mock.health_check()
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_unhealthy(self, client_with_mock, mock_httpx_client):
|
||||
"""Test health check returns false on error."""
|
||||
mock_httpx_client.get.side_effect = httpx.ConnectError("Connection refused")
|
||||
|
||||
result = await client_with_mock.health_check()
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestResponseModels:
|
||||
"""Tests for response model validation."""
|
||||
|
||||
def test_wiki_page_model(self):
|
||||
"""Test WikiPage model."""
|
||||
page = WikiPage(
|
||||
id=1,
|
||||
path="/test",
|
||||
title="Test Page",
|
||||
content="Content here",
|
||||
tags=["tag1"],
|
||||
)
|
||||
|
||||
assert page.id == 1
|
||||
assert page.title == "Test Page"
|
||||
|
||||
def test_wiki_page_optional_fields(self):
|
||||
"""Test WikiPage with minimal fields."""
|
||||
page = WikiPage(id=1, path="/test", title="Test")
|
||||
|
||||
assert page.content is None
|
||||
assert page.tags == []
|
||||
|
||||
def test_hybrid_search_result_model(self):
|
||||
"""Test HybridSearchResult model."""
|
||||
result = HybridSearchResult(
|
||||
source="vector",
|
||||
title="Title",
|
||||
content="Content",
|
||||
score=0.9,
|
||||
)
|
||||
|
||||
assert result.source == "vector"
|
||||
assert result.url is None
|
||||
assert result.metadata == {}
|
||||
|
||||
def test_vector_search_result_model(self):
|
||||
"""Test VectorSearchResult model."""
|
||||
result = VectorSearchResult(
|
||||
page_id=1,
|
||||
page_path="/doc",
|
||||
page_title="Doc",
|
||||
chunk_text="Text chunk",
|
||||
score=0.85,
|
||||
chunk_index=0,
|
||||
)
|
||||
|
||||
assert result.score == 0.85
|
||||
assert result.chunk_index == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUpdateWikiPage:
|
||||
"""Tests for update_wiki_page method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_wiki_page_content(self, client_with_mock, mock_httpx_client):
|
||||
"""Test updating wiki page content."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"id": 42,
|
||||
"path": "/docs/test",
|
||||
"title": "Test Page",
|
||||
"content": "# Updated\n\nNew content",
|
||||
"tags": ["test"],
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.put.return_value = mock_response
|
||||
|
||||
page = await client_with_mock.update_wiki_page(
|
||||
page_id=42,
|
||||
content="# Updated\n\nNew content",
|
||||
)
|
||||
|
||||
assert isinstance(page, WikiPage)
|
||||
assert page.id == 42
|
||||
assert "Updated" in page.content
|
||||
mock_httpx_client.put.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_wiki_page_tags_only(self, client_with_mock, mock_httpx_client):
|
||||
"""Test updating only tags (partial update)."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"id": 42,
|
||||
"path": "/docs/test",
|
||||
"title": "Test Page",
|
||||
"tags": ["projects", "devops"],
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.put.return_value = mock_response
|
||||
|
||||
page = await client_with_mock.update_wiki_page(
|
||||
page_id=42,
|
||||
tags=["projects", "devops"],
|
||||
)
|
||||
|
||||
assert page.tags == ["projects", "devops"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_wiki_page_multiple_fields(
|
||||
self, client_with_mock, mock_httpx_client
|
||||
):
|
||||
"""Test updating multiple fields at once."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"id": 42,
|
||||
"path": "/docs/test",
|
||||
"title": "New Title",
|
||||
"description": "New description",
|
||||
"tags": ["updated"],
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.put.return_value = mock_response
|
||||
|
||||
page = await client_with_mock.update_wiki_page(
|
||||
page_id=42,
|
||||
title="New Title",
|
||||
description="New description",
|
||||
tags=["updated"],
|
||||
)
|
||||
|
||||
assert page.title == "New Title"
|
||||
assert page.description == "New description"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSmartCreateWikiPage:
|
||||
"""Tests for smart_create_wiki_page method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_create_basic(self, client_with_mock, mock_httpx_client):
|
||||
"""Test basic smart create."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"page": {
|
||||
"id": 123,
|
||||
"path": "/users/test/technology/docker-compose",
|
||||
"title": "Docker Compose",
|
||||
"content": "# Docker Compose\n\nContent...",
|
||||
"tags": ["technology", "devops"],
|
||||
},
|
||||
"research_summary": {
|
||||
"wiki_results": 3,
|
||||
"web_results": 8,
|
||||
"graph_entities": 5,
|
||||
"keywords_extracted": 12,
|
||||
"timing_ms": 4500,
|
||||
},
|
||||
"sources_used": 11,
|
||||
"search_id": "uuid-123",
|
||||
"entity_linking": {
|
||||
"forward_links": 5,
|
||||
"backward_links": 3,
|
||||
"pages_updated": 2,
|
||||
},
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
result = await client_with_mock.smart_create_wiki_page(
|
||||
topic="Docker Compose",
|
||||
tags=["technology", "devops"],
|
||||
)
|
||||
|
||||
assert isinstance(result, SmartCreateResponse)
|
||||
assert result.page.id == 123
|
||||
assert result.page.title == "Docker Compose"
|
||||
assert result.sources_used == 11
|
||||
assert result.research_summary.wiki_results == 3
|
||||
assert result.research_summary.web_results == 8
|
||||
assert result.entity_linking.forward_links == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_smart_create_with_options(self, client_with_mock, mock_httpx_client):
|
||||
"""Test smart create with custom options."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
"page": {
|
||||
"id": 456,
|
||||
"path": "/custom/path",
|
||||
"title": "Custom Topic",
|
||||
"tags": ["custom"],
|
||||
},
|
||||
"research_summary": {
|
||||
"wiki_results": 5,
|
||||
"web_results": 0, # Web disabled
|
||||
"timing_ms": 2000,
|
||||
},
|
||||
"sources_used": 5,
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_httpx_client.post.return_value = mock_response
|
||||
|
||||
result = await client_with_mock.smart_create_wiki_page(
|
||||
topic="Custom Topic",
|
||||
tags=["custom"],
|
||||
path="/custom/path",
|
||||
include_web_research=False,
|
||||
)
|
||||
|
||||
assert result.page.path == "/custom/path"
|
||||
assert result.research_summary.web_results == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNewResponseModels:
|
||||
"""Tests for new response models."""
|
||||
|
||||
def test_research_summary_model(self):
|
||||
"""Test ResearchSummary model."""
|
||||
summary = ResearchSummary(
|
||||
wiki_results=3,
|
||||
web_results=5,
|
||||
graph_entities=2,
|
||||
keywords_extracted=10,
|
||||
timing_ms=3000,
|
||||
)
|
||||
|
||||
assert summary.wiki_results == 3
|
||||
assert summary.timing_ms == 3000
|
||||
|
||||
def test_research_summary_defaults(self):
|
||||
"""Test ResearchSummary default values."""
|
||||
summary = ResearchSummary()
|
||||
|
||||
assert summary.wiki_results == 0
|
||||
assert summary.timing_ms == 0
|
||||
|
||||
def test_entity_linking_model(self):
|
||||
"""Test EntityLinking model."""
|
||||
linking = EntityLinking(
|
||||
forward_links=5,
|
||||
backward_links=3,
|
||||
pages_updated=2,
|
||||
)
|
||||
|
||||
assert linking.forward_links == 5
|
||||
assert linking.pages_updated == 2
|
||||
|
||||
def test_smart_create_response_model(self):
|
||||
"""Test SmartCreateResponse model."""
|
||||
page = WikiPage(id=1, path="/test", title="Test")
|
||||
response = SmartCreateResponse(
|
||||
page=page,
|
||||
sources_used=10,
|
||||
search_id="uuid-456",
|
||||
)
|
||||
|
||||
assert response.page.id == 1
|
||||
assert response.sources_used == 10
|
||||
assert response.search_id == "uuid-456"
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
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"]
|
||||
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Tests for agent communication protocol.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.protocol import (
|
||||
AgentError,
|
||||
AgentRequest,
|
||||
AgentResponse,
|
||||
AgentTimeoutError,
|
||||
AgentUnavailableError,
|
||||
CoordinationResult,
|
||||
DelegationIntent,
|
||||
DelegationReason,
|
||||
ToolCallRecord,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentRequest:
|
||||
"""Tests for AgentRequest model."""
|
||||
|
||||
def test_basic_request(self):
|
||||
"""Test creating a basic agent request."""
|
||||
request = AgentRequest(task="Find information about Docker")
|
||||
|
||||
assert request.task == "Find information about Docker"
|
||||
assert request.context == ""
|
||||
assert request.timeout_seconds == 60
|
||||
|
||||
def test_request_with_context(self):
|
||||
"""Test request with additional context."""
|
||||
request = AgentRequest(
|
||||
task="Find Docker networking docs",
|
||||
context="User is setting up a homelab",
|
||||
delegation_reason=DelegationReason.DOMAIN_EXPERTISE,
|
||||
)
|
||||
|
||||
assert request.task == "Find Docker networking docs"
|
||||
assert request.context == "User is setting up a homelab"
|
||||
assert request.delegation_reason == DelegationReason.DOMAIN_EXPERTISE
|
||||
|
||||
def test_request_serialization(self):
|
||||
"""Test request can be serialized to dict."""
|
||||
request = AgentRequest(
|
||||
task="Research task",
|
||||
context="Some context",
|
||||
)
|
||||
|
||||
data = request.model_dump()
|
||||
|
||||
assert data["task"] == "Research task"
|
||||
assert data["context"] == "Some context"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentResponse:
|
||||
"""Tests for AgentResponse model."""
|
||||
|
||||
def test_successful_response(self):
|
||||
"""Test creating a successful response."""
|
||||
response = AgentResponse(
|
||||
success=True,
|
||||
result="Here are the findings...",
|
||||
reasoning="Searched wiki and found relevant docs",
|
||||
duration_ms=1500,
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
assert response.result == "Here are the findings..."
|
||||
assert response.reasoning == "Searched wiki and found relevant docs"
|
||||
assert response.duration_ms == 1500
|
||||
assert response.error_message is None
|
||||
|
||||
def test_failed_response(self):
|
||||
"""Test creating a failed response."""
|
||||
response = AgentResponse(
|
||||
success=False,
|
||||
result="",
|
||||
error_message="Connection timeout",
|
||||
duration_ms=30000,
|
||||
)
|
||||
|
||||
assert response.success is False
|
||||
assert response.result == ""
|
||||
assert response.error_message == "Connection timeout"
|
||||
|
||||
def test_response_with_tool_calls(self):
|
||||
"""Test response tracking tool calls."""
|
||||
tool_call = ToolCallRecord(
|
||||
tool_name="hybrid_search",
|
||||
arguments={"query": "Docker networking"},
|
||||
result="Found 5 results",
|
||||
duration_ms=500,
|
||||
)
|
||||
|
||||
response = AgentResponse(
|
||||
success=True,
|
||||
result="Based on search...",
|
||||
tool_calls=[tool_call],
|
||||
)
|
||||
|
||||
assert len(response.tool_calls) == 1
|
||||
assert response.tool_calls[0].tool_name == "hybrid_search"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDelegationIntent:
|
||||
"""Tests for DelegationIntent model."""
|
||||
|
||||
def test_basic_intent(self):
|
||||
"""Test creating a basic delegation intent."""
|
||||
intent = DelegationIntent(
|
||||
target_agent="librarian",
|
||||
task="Research Docker networking",
|
||||
reason=DelegationReason.DOMAIN_EXPERTISE,
|
||||
expected_outcome="Documentation and examples",
|
||||
)
|
||||
|
||||
assert intent.target_agent == "librarian"
|
||||
assert intent.task == "Research Docker networking"
|
||||
assert intent.reason == DelegationReason.DOMAIN_EXPERTISE
|
||||
assert intent.priority == 1 # Default
|
||||
|
||||
def test_intent_with_priority(self):
|
||||
"""Test intent with custom priority."""
|
||||
intent = DelegationIntent(
|
||||
target_agent="librarian",
|
||||
task="Urgent research",
|
||||
reason=DelegationReason.RESOURCE_EFFICIENCY,
|
||||
expected_outcome="Quick answer",
|
||||
priority=1,
|
||||
)
|
||||
|
||||
assert intent.priority == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDelegationReason:
|
||||
"""Tests for DelegationReason enum."""
|
||||
|
||||
def test_all_reasons_have_values(self):
|
||||
"""Test all delegation reasons are defined."""
|
||||
reasons = list(DelegationReason)
|
||||
|
||||
assert DelegationReason.DOMAIN_EXPERTISE in reasons
|
||||
assert DelegationReason.TOOL_ACCESS in reasons
|
||||
assert DelegationReason.RESOURCE_EFFICIENCY in reasons
|
||||
assert DelegationReason.USER_PREFERENCE in reasons
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCoordinationResult:
|
||||
"""Tests for CoordinationResult model."""
|
||||
|
||||
def test_single_agent_result(self):
|
||||
"""Test coordination with single agent."""
|
||||
agent_response = AgentResponse(
|
||||
success=True,
|
||||
result="Research findings",
|
||||
duration_ms=1000,
|
||||
)
|
||||
|
||||
intent = DelegationIntent(
|
||||
target_agent="librarian",
|
||||
task="Research task",
|
||||
reason=DelegationReason.DOMAIN_EXPERTISE,
|
||||
expected_outcome="Findings",
|
||||
)
|
||||
|
||||
result = CoordinationResult(
|
||||
final_response="Research findings",
|
||||
agent_responses={"librarian": agent_response},
|
||||
delegation_intents=[intent],
|
||||
total_duration_ms=1200,
|
||||
agents_consulted=["librarian"],
|
||||
)
|
||||
|
||||
assert result.final_response == "Research findings"
|
||||
assert len(result.agent_responses) == 1
|
||||
assert result.agents_consulted == ["librarian"]
|
||||
|
||||
def test_empty_result(self):
|
||||
"""Test coordination with no delegations."""
|
||||
result = CoordinationResult(
|
||||
final_response="",
|
||||
agent_responses={},
|
||||
delegation_intents=[],
|
||||
total_duration_ms=0,
|
||||
agents_consulted=[],
|
||||
)
|
||||
|
||||
assert result.final_response == ""
|
||||
assert len(result.agents_consulted) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentErrors:
|
||||
"""Tests for agent error types."""
|
||||
|
||||
def test_agent_error(self):
|
||||
"""Test base AgentError."""
|
||||
error = AgentError("Something went wrong")
|
||||
|
||||
assert "Something went wrong" in str(error)
|
||||
assert error.agent_name == "unknown"
|
||||
|
||||
def test_agent_timeout_error(self):
|
||||
"""Test AgentTimeoutError."""
|
||||
error = AgentTimeoutError(
|
||||
"Timed out after 60s",
|
||||
agent_name="librarian",
|
||||
)
|
||||
|
||||
assert "Timed out" in str(error)
|
||||
assert error.agent_name == "librarian"
|
||||
|
||||
def test_agent_unavailable_error(self):
|
||||
"""Test AgentUnavailableError."""
|
||||
error = AgentUnavailableError(
|
||||
"Agent not registered",
|
||||
agent_name="unknown_agent",
|
||||
)
|
||||
|
||||
assert "not registered" in str(error)
|
||||
assert error.agent_name == "unknown_agent"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestToolCallRecord:
|
||||
"""Tests for ToolCallRecord model."""
|
||||
|
||||
def test_tool_call_record(self):
|
||||
"""Test creating a tool call record."""
|
||||
record = ToolCallRecord(
|
||||
tool_name="semantic_search",
|
||||
arguments={"query": "networking concepts", "limit": 10},
|
||||
result="Found 10 relevant documents",
|
||||
duration_ms=250,
|
||||
)
|
||||
|
||||
assert record.tool_name == "semantic_search"
|
||||
assert record.arguments["query"] == "networking concepts"
|
||||
assert record.duration_ms == 250
|
||||
|
||||
def test_tool_call_with_empty_result(self):
|
||||
"""Test tool call with empty result."""
|
||||
record = ToolCallRecord(
|
||||
tool_name="query_graph",
|
||||
arguments={"cypher": "MATCH (n) RETURN n"},
|
||||
result="",
|
||||
duration_ms=100,
|
||||
)
|
||||
|
||||
assert record.result == ""
|
||||
Reference in New Issue
Block a user