- **New Class Structure**: Introduced `ToolManager` and `AsyncToolManager` classes (`ArcadeToolManager` is deprecated) - **Async Support**: Full async implementation for modern LangChain applications - **Better Tool Management**: New methods for adding individual tools and toolkits - **CI/CD**: for langchain_arcade ## Upgrade Changes ```python # Old pattern manager = ArcadeToolManager(api_key="...") tools = manager.get_tools(toolkits=["Google"]) # New pattern manager = ToolManager(api_key="...") manager.init_tools(toolkits=["Google"]) tools = manager.to_langchain() ``` Now supports underscores vs dots in tool names for better model compatibility.
706 lines
23 KiB
Python
706 lines
23 KiB
Python
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from arcadepy import NOT_GIVEN
|
|
from arcadepy.pagination import AsyncOffsetPage, SyncOffsetPage
|
|
from arcadepy.types import ToolDefinition
|
|
from arcadepy.types.shared import AuthorizationResponse
|
|
from langchain_arcade.manager import ArcadeToolManager, AsyncToolManager, ToolManager
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_arcade_client():
|
|
"""
|
|
A fixture to mock the Arcade client object for testing the ToolManager.
|
|
|
|
This mocks all relevant methods used by the manager, including:
|
|
- tools.get
|
|
- tools.list
|
|
- tools.authorize
|
|
- auth.status
|
|
- auth.wait_for_completion
|
|
"""
|
|
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()
|
|
mock_client.auth.wait_for_completion = MagicMock()
|
|
|
|
return mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def async_mock_arcade_client():
|
|
"""
|
|
A fixture to mock the Arcade client object for testing the AsyncToolManager.
|
|
"""
|
|
mock_client = AsyncMock()
|
|
mock_client.tools.get = AsyncMock()
|
|
mock_client.tools.list = AsyncMock()
|
|
mock_client.tools.authorize = AsyncMock()
|
|
mock_client.auth.status = AsyncMock()
|
|
mock_client.auth.wait_for_completion = AsyncMock()
|
|
return mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def manager(mock_arcade_client):
|
|
"""
|
|
A fixture that creates a ToolManager with the mocked Arcade client.
|
|
"""
|
|
return ToolManager(client=mock_arcade_client)
|
|
|
|
|
|
@pytest.fixture
|
|
def async_manager(async_mock_arcade_client):
|
|
"""
|
|
A fixture that creates an AsyncToolManager with the mocked Arcade client.
|
|
"""
|
|
return AsyncToolManager(client=async_mock_arcade_client)
|
|
|
|
|
|
@pytest.fixture(params=[("sync", False), ("async", True)])
|
|
def manager_fixture(request, manager, async_manager):
|
|
"""
|
|
A parameterized fixture that returns a tuple with:
|
|
- The appropriate manager (sync or async)
|
|
- A boolean indicating if it's async
|
|
- The appropriate mock client
|
|
"""
|
|
param_name, is_async = request.param
|
|
if is_async:
|
|
return async_manager, True
|
|
else:
|
|
return manager, False
|
|
|
|
|
|
@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
|
|
|
|
|
|
async def maybe_await(obj, is_async):
|
|
"""Helper to handle both sync and async return values"""
|
|
if is_async:
|
|
return await obj
|
|
return obj
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_init_tools_parameterized(
|
|
manager_fixture, mock_arcade_client, async_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
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_tool = make_tool("Search_SearchGoogle")
|
|
client.tools.get.return_value = mock_tool
|
|
|
|
page_cls = AsyncOffsetPage if is_async else SyncOffsetPage
|
|
client.tools.list.return_value = page_cls(items=[mock_tool])
|
|
|
|
# Act
|
|
result = await maybe_await(manager.init_tools(tools=["Search_SearchGoogle"]), is_async)
|
|
|
|
# Assert
|
|
assert "Search_SearchGoogle" in manager.tools
|
|
assert manager._tools["Search_SearchGoogle"] == mock_tool
|
|
client.tools.get.assert_called_once_with(name="Search_SearchGoogle")
|
|
# Verify the result is a list of StructuredTool objects
|
|
assert len(result) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_to_langchain_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that to_langchain returns the tools as StructuredTool objects.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
|
|
mock_tool = make_tool("Search_SearchGoogle")
|
|
manager._tools = {"Search_SearchGoogle": mock_tool}
|
|
|
|
# Act - with default parameters
|
|
result = await maybe_await(manager.to_langchain(), is_async)
|
|
|
|
# Assert
|
|
assert len(result) == 1
|
|
assert result[0].name == "Search_SearchGoogle"
|
|
|
|
# Act - with underscores=False
|
|
result = await maybe_await(manager.to_langchain(use_underscores=False), is_async)
|
|
|
|
# Assert
|
|
assert len(result) == 1
|
|
assert result[0].name == "Search.SearchGoogle"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deprecated_get_tools_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that the deprecated get_tools method still works but issues a warning.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_tool = make_tool("Search_SearchGoogle")
|
|
client.tools.get.return_value = mock_tool
|
|
manager._tools = {} # Ensure no tools are already loaded
|
|
|
|
# Act - Check for deprecation warning
|
|
with pytest.warns(DeprecationWarning):
|
|
result = await maybe_await(manager.get_tools(tools=["Search_SearchGoogle"]), is_async)
|
|
|
|
# Assert - Method should still work
|
|
assert len(result) == 1
|
|
assert "Search_SearchGoogle" in manager.tools
|
|
client.tools.get.assert_called_once_with(name="Search_SearchGoogle")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_tool_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that add_tool adds a single tool to the manager without clearing existing tools.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
# Set up two different mock tools
|
|
mock_tool_google = make_tool("Search_SearchGoogle")
|
|
mock_tool_bing = make_tool("Search_SearchBing")
|
|
|
|
# First tool already exists in manager
|
|
manager._tools = {"Search_SearchGoogle": mock_tool_google}
|
|
|
|
# Second tool will be added
|
|
client.tools.get.return_value = mock_tool_bing
|
|
|
|
# Act
|
|
await maybe_await(manager.add_tool("Search_SearchBing"), is_async)
|
|
|
|
# Assert - Both tools should now be in the manager
|
|
assert "Search_SearchGoogle" in manager.tools
|
|
assert "Search_SearchBing" in manager.tools
|
|
assert len(manager.tools) == 2
|
|
client.tools.get.assert_called_once_with(name="Search_SearchBing")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_toolkit_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that add_toolkit adds all tools from a toolkit without clearing existing tools.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
# Create a tool that's already in the manager
|
|
mock_tool_google = make_tool("Search_SearchGoogle")
|
|
manager._tools = {"Search_SearchGoogle": mock_tool_google}
|
|
|
|
# Create tools to be added from the toolkit
|
|
mock_tool_bing = make_tool("Search_SearchBing")
|
|
mock_tool_ddg = make_tool("Search_SearchDuckDuckGo")
|
|
|
|
# Mock the response for toolkit listing
|
|
page_cls = AsyncOffsetPage if is_async else SyncOffsetPage
|
|
client.tools.list.return_value = page_cls(items=[mock_tool_bing, mock_tool_ddg])
|
|
|
|
# Act
|
|
await maybe_await(manager.add_toolkit("Search"), is_async)
|
|
|
|
# Assert - All tools should now be in the manager
|
|
assert len(manager.tools) == 3
|
|
assert "Search_SearchGoogle" in manager.tools
|
|
assert "Search_SearchBing" in manager.tools
|
|
assert "Search_SearchDuckDuckGo" in manager.tools
|
|
client.tools.list.assert_called_once_with(toolkit="Search", limit=NOT_GIVEN, offset=NOT_GIVEN)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_authorized_with_response_object_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client
|
|
):
|
|
"""
|
|
Test the is_authorized method accepting both authorization ID string and AuthorizationResponse.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_type = AsyncMock if is_async else MagicMock
|
|
client.auth.status.return_value = mock_type(status="completed")
|
|
|
|
# Create an auth response object
|
|
auth_response = AuthorizationResponse(
|
|
id="auth_abc", status="pending", tool_fully_qualified_name="Search_SearchGoogle"
|
|
)
|
|
|
|
# Act - Test with string ID
|
|
status_result1 = await maybe_await(manager.is_authorized("auth_abc"), is_async)
|
|
|
|
# Act - Test with response object
|
|
status_result2 = await maybe_await(manager.is_authorized(auth_response), is_async)
|
|
|
|
# Assert
|
|
assert status_result1 is True
|
|
assert status_result2 is True
|
|
client.auth.status.assert_any_call(id="auth_abc")
|
|
client.auth.status.assert_any_call(
|
|
id="auth_abc"
|
|
) # Should be called with the same ID both times
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wait_for_auth_with_response_object_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client
|
|
):
|
|
"""
|
|
Test the wait_for_auth method accepting both authorization ID string and AuthorizationResponse.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
completed_response = AuthorizationResponse(
|
|
id="auth_abc", status="completed", tool_fully_qualified_name="Search_SearchGoogle"
|
|
)
|
|
client.auth.wait_for_completion.return_value = completed_response
|
|
|
|
# Create an auth response object
|
|
auth_response = AuthorizationResponse(
|
|
id="auth_abc", status="pending", tool_fully_qualified_name="Search_SearchGoogle"
|
|
)
|
|
|
|
# Act - Test with string ID
|
|
result1 = await maybe_await(manager.wait_for_auth("auth_abc"), is_async)
|
|
|
|
# Act - Test with response object
|
|
result2 = await maybe_await(manager.wait_for_auth(auth_response), is_async)
|
|
|
|
# Assert
|
|
assert result1 == completed_response
|
|
assert result2 == completed_response
|
|
client.auth.wait_for_completion.assert_any_call("auth_abc")
|
|
client.auth.wait_for_completion.assert_any_call(
|
|
"auth_abc"
|
|
) # Should be called with the same ID both times
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_tools_no_init_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that the deprecated get_tools method without previous initialization
|
|
issues a warning and fetches tools.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_tool = make_tool("Search_SearchGoogle")
|
|
page_cls = AsyncOffsetPage if is_async else SyncOffsetPage
|
|
client.tools.list.return_value = page_cls(items=[mock_tool])
|
|
|
|
# Act - Check for deprecation warning
|
|
with pytest.warns(DeprecationWarning):
|
|
tools = await maybe_await(
|
|
manager.get_tools(), is_async
|
|
) # No param means manager calls list
|
|
|
|
# Assert
|
|
assert len(tools) == 0
|
|
assert "Search_SearchGoogle" not in manager.tools
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_tools_with_explicit_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that the deprecated get_tools method with explicitly specified tools
|
|
issues a warning and fetches the requested tools.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_tool_google = make_tool("Search_SearchGoogle")
|
|
mock_tool_bing = make_tool("Search_SearchBing")
|
|
client.tools.get.side_effect = [mock_tool_google, mock_tool_bing]
|
|
|
|
# Act - Check for deprecation warning
|
|
with pytest.warns(DeprecationWarning):
|
|
retrieved_tools = await maybe_await(
|
|
manager.get_tools(tools=["Search_SearchGoogle", "Search_SearchBing"]), is_async
|
|
)
|
|
|
|
# Assert
|
|
assert len(retrieved_tools) == 2
|
|
assert set(manager.tools) == {"Search_SearchGoogle", "Search_SearchBing"}
|
|
client.tools.get.assert_any_call(name="Search_SearchGoogle")
|
|
client.tools.get.assert_any_call(name="Search_SearchBing")
|
|
|
|
|
|
def test_arcade_tool_manager_deprecation_warning():
|
|
"""
|
|
Test that the ArcadeToolManager class issues a deprecation warning.
|
|
"""
|
|
# Act - Check for deprecation warning
|
|
with pytest.warns(DeprecationWarning) as warnings_record:
|
|
ArcadeToolManager(client=MagicMock())
|
|
# Assert
|
|
assert any("ArcadeToolManager is deprecated" in str(w.message) for w in warnings_record)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_authorize_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client
|
|
):
|
|
"""
|
|
Test the authorize method to ensure it calls the Arcade client's
|
|
tools.authorize method correctly.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
auth_response = AuthorizationResponse(
|
|
id="auth_123", status="pending", tool_fully_qualified_name="Search_SearchGoogle"
|
|
)
|
|
client.tools.authorize.return_value = auth_response
|
|
|
|
# Act
|
|
response = await maybe_await(
|
|
manager.authorize(tool_name="Search_SearchGoogle", user_id="user_123"), is_async
|
|
)
|
|
|
|
# Assert
|
|
assert response.id == "auth_123"
|
|
assert response.status == "pending"
|
|
client.tools.authorize.assert_called_once_with(
|
|
tool_name="Search_SearchGoogle", user_id="user_123"
|
|
)
|
|
|
|
|
|
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 results[0].fully_qualified_name == "Search_SearchGoogle"
|
|
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 results[0].fully_qualified_name == "Search_SearchBing"
|
|
mock_arcade_client.tools.list.assert_called_once_with(
|
|
toolkit="Search", limit=NOT_GIVEN, offset=NOT_GIVEN
|
|
)
|
|
|
|
|
|
def test_retrieve_tool_definitions_raise_on_empty(manager):
|
|
"""
|
|
Test that _retrieve_tool_definitions raises ValueError when no tools or toolkits
|
|
are provided and raise_on_empty is True.
|
|
"""
|
|
# Act & Assert
|
|
with pytest.raises(ValueError) as excinfo:
|
|
manager._retrieve_tool_definitions(tools=None, toolkits=None, raise_on_empty=True)
|
|
|
|
assert "No tools or toolkits provided" in str(excinfo.value)
|
|
|
|
|
|
def test_retrieve_tool_definitions_empty_no_raise(manager):
|
|
"""
|
|
Test that _retrieve_tool_definitions returns empty list when no tools or toolkits
|
|
are provided and raise_on_empty is False.
|
|
"""
|
|
# Act
|
|
results = manager._retrieve_tool_definitions(tools=None, toolkits=None, raise_on_empty=False)
|
|
|
|
# Assert
|
|
assert results == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retrieve_tool_definitions_with_limit_offset_parameterized(
|
|
manager_fixture, mock_arcade_client, async_mock_arcade_client, make_tool
|
|
):
|
|
"""
|
|
Test that _retrieve_tool_definitions respects limit and offset parameters.
|
|
"""
|
|
# Arrange
|
|
manager, is_async = manager_fixture
|
|
client = async_mock_arcade_client if is_async else mock_arcade_client
|
|
|
|
mock_tool = make_tool("Search_SearchGoogle")
|
|
page_cls = AsyncOffsetPage if is_async else SyncOffsetPage
|
|
client.tools.list.return_value = page_cls(items=[mock_tool])
|
|
|
|
# Act
|
|
if is_async:
|
|
results = await manager._retrieve_tool_definitions(toolkits=["Search"], limit=10, offset=5)
|
|
else:
|
|
results = manager._retrieve_tool_definitions(toolkits=["Search"], limit=10, offset=5)
|
|
|
|
# Assert
|
|
assert len(results) > 0
|
|
client.tools.list.assert_called_once_with(toolkit="Search", limit=10, offset=5)
|
|
|
|
|
|
def test_get_client_config_with_kwargs():
|
|
"""
|
|
Test that _get_client_config prioritizes kwargs over environment variables.
|
|
"""
|
|
# Arrange
|
|
manager = ToolManager(client=MagicMock()) # Client won't be used here
|
|
|
|
# Act
|
|
with patch.dict("os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}):
|
|
result = manager._get_client_config(api_key="kwarg_key", base_url="kwarg_url")
|
|
|
|
# Assert
|
|
assert result["api_key"] == "kwarg_key"
|
|
assert result["base_url"] == "kwarg_url"
|
|
|
|
|
|
def test_get_client_config_with_env_vars():
|
|
"""
|
|
Test that _get_client_config falls back to environment variables when kwargs not provided.
|
|
"""
|
|
# Arrange
|
|
manager = ToolManager(client=MagicMock()) # Client won't be used here
|
|
|
|
# Act
|
|
with patch.dict("os.environ", {"ARCADE_API_KEY": "env_key", "ARCADE_BASE_URL": "env_url"}):
|
|
result = manager._get_client_config()
|
|
|
|
# Assert
|
|
assert result["api_key"] == "env_key"
|
|
assert result["base_url"] == "env_url"
|
|
|
|
|
|
def test_getitem_access(manager, make_tool):
|
|
"""
|
|
Test that __getitem__ allows dictionary-style access to tools.
|
|
"""
|
|
# Arrange
|
|
tool_name = "Search_SearchGoogle"
|
|
mock_tool_def = make_tool(tool_name)
|
|
manager._tools[tool_name] = mock_tool_def
|
|
|
|
# Act
|
|
definition = manager[tool_name]
|
|
|
|
# Assert
|
|
assert definition == mock_tool_def
|
|
|
|
|
|
def test_getitem_missing(manager):
|
|
"""
|
|
Test that __getitem__ raises ValueError for missing tools.
|
|
"""
|
|
# Act & Assert
|
|
with pytest.raises(ValueError) as excinfo:
|
|
_ = manager["Nonexistent.Tool"]
|
|
|
|
assert "Tool 'Nonexistent.Tool' not found" in str(excinfo.value)
|
|
|
|
|
|
def test_create_tool_map_with_underscores(make_tool):
|
|
"""
|
|
Test the _create_tool_map function with use_underscores=True.
|
|
"""
|
|
# Arrange
|
|
from langchain_arcade.manager import _create_tool_map
|
|
|
|
tool1 = make_tool("Search.SearchGoogle")
|
|
tool2 = make_tool("Gmail.SendEmail")
|
|
|
|
# Act
|
|
result = _create_tool_map([tool1, tool2], use_underscores=True)
|
|
|
|
# Assert
|
|
assert "Search_SearchGoogle" in result
|
|
assert "Gmail_SendEmail" in result
|
|
assert len(result) == 2
|
|
|
|
|
|
def test_create_tool_map_with_dots(make_tool):
|
|
"""
|
|
Test the _create_tool_map function with use_underscores=False.
|
|
"""
|
|
# Arrange
|
|
from langchain_arcade.manager import _create_tool_map
|
|
|
|
tool1 = make_tool("Search.SearchGoogle")
|
|
tool2 = make_tool("Gmail.SendEmail")
|
|
|
|
# Act
|
|
result = _create_tool_map([tool1, tool2], use_underscores=False)
|
|
|
|
# Assert
|
|
assert "Search.SearchGoogle" in result
|
|
assert "Gmail.SendEmail" in result
|
|
assert len(result) == 2
|