arcade-mcp/contrib/langchain/tests/test_manager.py

311 lines
10 KiB
Python

from unittest.mock import MagicMock
import pytest
from arcadepy.pagination import SyncOffsetPage
from arcadepy.types import ToolDefinition
from arcadepy.types.shared import AuthorizationResponse
from langchain_arcade.manager import ArcadeToolManager
@pytest.fixture
def mock_arcade_client():
"""
A fixture to mock the Arcade client object for testing the ArcadeToolManager.
This mocks all relevant methods used by the manager, including:
- tools.get
- tools.list
- tools.authorize
- auth.status
"""
mock_client = MagicMock()
# Mock the "tools" sub-client
mock_client.tools.get = MagicMock()
mock_client.tools.list = MagicMock()
mock_client.tools.authorize = MagicMock()
# Mock the "auth" sub-client
mock_client.auth.status = MagicMock()
return mock_client
@pytest.fixture
def manager(mock_arcade_client):
"""
A fixture that creates an ArcadeToolManager with the mocked Arcade client.
"""
return ArcadeToolManager(client=mock_arcade_client)
@pytest.fixture
def make_tool():
"""
A factory fixture for creating a valid ToolDefinition with a given
fully qualified name. Because the underlying ToolDefinition model
expects "toolkit" to be a dictionary with at least one field (for example "slug"),
and "requirements.authorization" to be a valid dictionary if present, we set them up
accordingly.
"""
def _make_tool(fully_qualified_name="Search_SearchGoogle", **kwargs):
# Split on the first dot to derive a 'toolkit' slug and a tool 'name'
if "." in fully_qualified_name:
raw_toolkit, raw_tool_name = fully_qualified_name.split(".", 1)
elif "_" in fully_qualified_name:
# Convert from "_" to "." to match the expected format of tool name when
# using Langchain models for LLM inference.
raw_toolkit, raw_tool_name = fully_qualified_name.split("_", 1)
else:
raw_toolkit, raw_tool_name = fully_qualified_name, fully_qualified_name
# Provide a default toolkit dict unless one already exists in kwargs
toolkit = kwargs.pop("toolkit", {"name": raw_toolkit})
# Provide a default input
# arcadepy.types.ToolDefinition expects "input" to be a valid structure (dict).
tool_input = kwargs.pop("input", {"parameters": []})
# Convert MagicMock-based requirements (with authorization) to an appropriate dict,
# or use what's passed. If none is passed, default to None.
requirements = kwargs.pop("requirements", None)
if requirements is not None and not isinstance(requirements, dict):
# If it's e.g. a MagicMock(authorization="xyz"), convert it to a dict
req_auth = getattr(requirements, "authorization", None)
# If the test expects an authorization presence, represent it as a dict
# that Pydantic can parse
if req_auth is not None:
requirements = {"authorization": {"type": req_auth}}
else:
requirements = {"authorization": None}
# Provide a default description if none is supplied
description = kwargs.pop("description", "Mock tool for testing")
# Build the pydantic fields
data = {
"fully_qualified_name": fully_qualified_name,
"name": raw_tool_name,
"toolkit": toolkit,
"input": tool_input,
"description": description,
"requirements": requirements,
}
data.update(kwargs) # merge any extras
return ToolDefinition(**data)
return _make_tool
def test_init_tools(manager, mock_arcade_client, make_tool):
"""
Test that init_tools clears any existing tools and retrieves new ones
from either an explicit list of tools or an entire toolkit.
"""
# Arrange
mock_tool = make_tool("Search_SearchGoogle")
mock_arcade_client.tools.get.return_value = mock_tool
mock_arcade_client.tools.list.return_value = SyncOffsetPage(items=[mock_tool])
# Act
manager.init_tools(tools=["Search_SearchGoogle"])
# Assert
assert "Search_SearchGoogle" in manager.tools
assert manager._tools["Search_SearchGoogle"] == mock_tool
mock_arcade_client.tools.get.assert_called_once_with(name="Search_SearchGoogle")
def test_get_tools_no_init(manager, mock_arcade_client, make_tool):
"""
If get_tools is called without init_tools and no tools are specified,
it should call init_tools internally and fetch all available tools.
"""
# Arrange
mock_tool = make_tool("Search_SearchGoogle")
mock_arcade_client.tools.list.return_value = SyncOffsetPage(items=[mock_tool])
# Act
tools = manager.get_tools() # no param means manager auto-inits
# Assert
assert len(tools) == 1
assert "Search_SearchGoogle" in manager.tools
assert manager._tools["Search_SearchGoogle"] == mock_tool
mock_arcade_client.tools.list.assert_called_once()
def test_get_tools_with_explicit(manager, mock_arcade_client, make_tool):
"""
If tools or toolkits are provided to get_tools, the manager should
retrieve or update the internal _tools dictionary accordingly,
then return them as StructuredTool objects.
"""
# Arrange
mock_tool_google = make_tool("Search_SearchGoogle")
mock_tool_bing = make_tool("Search_SearchBing")
mock_arcade_client.tools.get.side_effect = [mock_tool_google, mock_tool_bing]
# Act
retrieved_tools = manager.get_tools(tools=["Search_SearchGoogle", "Search_SearchBing"])
# Assert
assert len(retrieved_tools) == 2
assert set(manager.tools) == {"Search_SearchGoogle", "Search_SearchBing"}
mock_arcade_client.tools.get.assert_any_call(name="Search_SearchGoogle")
mock_arcade_client.tools.get.assert_any_call(name="Search_SearchBing")
def test_authorize(manager, mock_arcade_client):
"""
Test the authorize method to ensure it calls the Arcade client's
tools.authorize method correctly.
"""
# Arrange
mock_arcade_client.tools.authorize.return_value = AuthorizationResponse(
id="auth_123", status="pending", tool_fully_qualified_name="Search_SearchGoogle"
)
# Act
response = manager.authorize(tool_name="Search_SearchGoogle", user_id="user_123")
# Assert
assert response.id == "auth_123"
assert response.status == "pending"
mock_arcade_client.tools.authorize.assert_called_once_with(
tool_name="Search_SearchGoogle", user_id="user_123"
)
def test_is_authorized(manager, mock_arcade_client):
"""
Test the is_authorized method which checks if authorization
has completed for a given authorization ID.
"""
# Arrange
mock_arcade_client.auth.status.return_value = MagicMock(status="completed")
# Act
status_result = manager.is_authorized("auth_abc")
# Assert
assert status_result is True
mock_arcade_client.auth.status.assert_called_once_with(id="auth_abc")
def test_requires_auth_true(manager, make_tool):
"""
Test the requires_auth method returning True if
the stored tool definition's requirements contain an authorization entry.
"""
# Arrange
tool_name = "Search_SearchGoogle"
# Pass a MagicMock with 'authorization' to ensure it gets converted
mock_tool_def = make_tool(tool_name, requirements=MagicMock(authorization="some_required_auth"))
manager._tools[tool_name] = mock_tool_def
# Act
result = manager.requires_auth(tool_name)
# Assert
assert result is True
def test_requires_auth_false(manager, make_tool):
"""
Test the requires_auth method returning False if authorization
is not required in the tool definition.
"""
# Arrange
tool_name = "Search_SearchGoogle"
mock_tool_def = make_tool(tool_name, requirements=MagicMock(authorization=None))
manager._tools[tool_name] = mock_tool_def
# Act
result = manager.requires_auth(tool_name)
# Assert
assert result is False
def test_get_tool_definition_existing(manager, make_tool):
"""
Test the internal _get_tool_definition method retrieving
an existing tool definition by name.
"""
# Arrange
tool_name = "Search_SearchGoogle"
mock_tool_def = make_tool(tool_name)
manager._tools[tool_name] = mock_tool_def
# Act
definition = manager._get_tool_definition(tool_name)
# Assert
assert definition == mock_tool_def
def test_get_tool_definition_missing(manager):
"""
Test the internal _get_tool_definition method raising a ValueError
if the tool is not in the manager.
"""
# Act & Assert
with pytest.raises(ValueError) as excinfo:
manager._get_tool_definition("Nonexistent.Tool")
assert "Tool 'Nonexistent.Tool' not found" in str(excinfo.value)
def test_retrieve_tool_definitions_tools_only(manager, mock_arcade_client, make_tool):
"""
Test the internal _retrieve_tool_definitions method by specifying tools only.
"""
# Arrange
mock_tool = make_tool("Search_SearchGoogle")
mock_arcade_client.tools.get.return_value = mock_tool
# Act
results = manager._retrieve_tool_definitions(tools=["Search_SearchGoogle"], toolkits=None)
# Assert
assert len(results) == 1
assert "Search_SearchGoogle" in results
mock_arcade_client.tools.get.assert_called_once_with(name="Search_SearchGoogle")
def test_retrieve_tool_definitions_toolkits_only(manager, mock_arcade_client, make_tool):
"""
Test the internal _retrieve_tool_definitions method by specifying toolkits.
"""
# Arrange
mock_tool = make_tool("Search_SearchBing")
mock_arcade_client.tools.list.return_value = SyncOffsetPage(items=[mock_tool])
# Act
results = manager._retrieve_tool_definitions(tools=None, toolkits=["Search"])
# Assert
assert len(results) == 1
assert "Search_SearchBing" in results
mock_arcade_client.tools.list.assert_called_once_with(toolkit="Search")
def test_retrieve_tool_definitions_no_args(manager, mock_arcade_client, make_tool):
"""
Test the internal _retrieve_tool_definitions method when no
arguments are provided, retrieving all available tools.
"""
# Arrange
mock_tool1 = make_tool("Search_SearchGoogle")
mock_tool2 = make_tool("Search_SearchBing")
mock_arcade_client.tools.list.return_value = SyncOffsetPage(items=[mock_tool1, mock_tool2])
# Act
results = manager._retrieve_tool_definitions()
# Assert
assert len(results) == 2
assert "Search_SearchGoogle" in results
assert "Search_SearchBing" in results
mock_arcade_client.tools.list.assert_called_once()