Files
composiohq--composio/python/tests/test_tool_execution.py
T
2026-07-13 12:38:34 +08:00

1288 lines
47 KiB
Python

"""Test tool execution with toolkit versions and argument serialization."""
import typing as t
from unittest.mock import Mock, patch
import pytest
from pydantic import BaseModel, RootModel
from composio_client import omit
from composio.client.types import Tool, tool_list_response
from composio.core.models.base import allow_tracking
from composio.core.models.tools import Tools, _serialize_arguments, _needs_serialization
from composio.exceptions import ToolVersionRequiredError
from tests.conftest import mock_http_client
@pytest.fixture(autouse=True)
def disable_telemetry():
"""Disable telemetry for all tests to prevent thread issues."""
token = allow_tracking.set(False)
yield
allow_tracking.reset(token)
class TestToolExecution:
"""Test cases for tool execution with toolkit versions."""
def create_mock_tool(self, slug: str, toolkit_slug: str) -> Tool:
"""Create a mock tool for testing."""
return Tool(
name=f"Test {slug}",
slug=slug,
description="Test tool",
input_parameters={},
output_parameters={},
available_versions=["v1.0.0"],
version="v1.0.0",
scopes=[],
toolkit=tool_list_response.ItemToolkit(
name=toolkit_slug.title(), slug=toolkit_slug, logo=""
),
deprecated=tool_list_response.ItemDeprecated(
available_versions=["v1.0.0"],
displayName=f"Test {slug}",
version="v1.0.0",
toolkit=tool_list_response.ItemDeprecatedToolkit(logo=""),
is_deprecated=False,
),
is_deprecated=False,
no_auth=False,
tags=[],
)
def test_get_raw_tool_router_meta_tools_fetches_all_pages(self):
"""Test that ToolRouter session tools are fetched across all pages."""
mock_client = mock_http_client()
mock_provider = Mock()
tools = Tools(client=mock_client, provider=mock_provider)
first_tool = self.create_mock_tool("FIRST_TOOL", "github")
second_tool = self.create_mock_tool("SECOND_TOOL", "slack")
first_response = Mock(items=[first_tool], next_cursor="next_page")
second_response = Mock(items=[second_tool], next_cursor=None)
mock_client.tool_router.session.tools.side_effect = [
first_response,
second_response,
]
result = tools.get_raw_tool_router_meta_tools("session_123")
assert [tool.slug for tool in result] == ["FIRST_TOOL", "SECOND_TOOL"]
mock_client.tool_router.session.tools.assert_any_call(
session_id="session_123",
cursor=omit,
limit=500,
)
mock_client.tool_router.session.tools.assert_any_call(
session_id="session_123",
limit=500,
cursor="next_page",
)
def test_tool_execution_uses_toolkit_version(self):
"""Test that tool execution resolves toolkit version correctly."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions configuration
toolkit_versions = {"github": "20251201_01", "slack": "20251201_02"}
# Create Tools instance
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug method
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute the tool
tools._execute_tool(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
# Verify that the client was called with the resolved version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
# Should have called with version="20251201_01" (resolved from github toolkit)
assert call_args.kwargs["version"] == "20251201_01"
def test_tool_execution_with_explicit_version(self):
"""Test that explicit version overrides toolkit version."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions configuration
toolkit_versions = {
"github": "20251201_01",
}
# Create Tools instance
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug method
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute the tool with explicit version
tools._execute_tool(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
version="20251201_03", # Explicit version should take precedence
)
# Verify that the client was called with the explicit version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
# Should have called with explicit version, not resolved from toolkit
assert call_args.kwargs["version"] == "20251201_03"
def test_tool_execution_unknown_toolkit_fallback(self):
"""Test that unknown toolkit falls back to 'latest'."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions configuration that doesn't include the tool's toolkit
toolkit_versions = {
"slack": "20251201_02" # No github version specified
}
# Create Tools instance
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool (not in toolkit_versions)
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug method
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute the tool with dangerously_skip_version_check since version will be 'latest'
tools._execute_tool(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
dangerously_skip_version_check=True,
)
# Verify that the client was called with "latest" (fallback)
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
# Should have called with "latest" since github is not in toolkit_versions
assert call_args.kwargs["version"] == "latest"
def test_tool_execution_with_global_version_string(self):
"""Test that global version string is used for all toolkits."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create global toolkit version (string)
toolkit_versions = "20251201_03"
# Create Tools instance
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug method
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute the tool
tools._execute_tool(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
# Verify that the client was called with the global version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
# Should have called with the global version string
assert call_args.kwargs["version"] == "20251201_03"
def test_tool_execution_matches_typescript_behavior(self):
"""Test that Python execution matches TypeScript behavior exactly."""
# This test verifies the same logic as TypeScript:
# version: body.version ?? getToolkitVersion(tool.toolkit?.slug ?? 'unknown', this.toolkitVersions)
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions like TypeScript would have
toolkit_versions = {
"github": "20251201_01",
"slack": "latest",
"notion": "20251201_05",
}
# Create Tools instance
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Test cases matching TypeScript behavior
test_cases = [
("GITHUB_GET_REPOS", "github", "20251201_01", False),
(
"SLACK_SEND_MESSAGE",
"slack",
"latest",
True,
), # Need skip flag for latest
("NOTION_CREATE_PAGE", "notion", "20251201_05", False),
(
"CUSTOM_TOOL",
"unknown_toolkit",
"latest",
True,
), # Unknown toolkit fallback to latest
]
for tool_slug, toolkit_slug, expected_version, needs_skip in test_cases:
# Create mock tool
mock_tool = self.create_mock_tool(tool_slug, toolkit_slug)
# Mock the get_raw_composio_tool_by_slug method
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=mock_tool
):
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute the tool
if needs_skip:
tools._execute_tool(
slug=tool_slug,
arguments={"test": "data"},
dangerously_skip_version_check=True,
)
else:
tools._execute_tool(
slug=tool_slug,
arguments={"test": "data"},
)
# Verify the version matches expected
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == expected_version, (
f"Tool {tool_slug} with toolkit {toolkit_slug} should use version {expected_version}"
)
# Reset mock for next iteration
mock_client.tools.execute.reset_mock()
def test_execute_raises_error_when_version_is_latest_without_skip_flag(self):
"""Test that execute raises ToolVersionRequiredError when version is 'latest' without skip flag."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance without toolkit versions (defaults to latest)
tools = Tools(
client=mock_client,
provider=mock_provider,
)
# Create a mock GitHub tool with proper input_parameters for file helper
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
github_tool.input_parameters = {"type": "object", "properties": {}}
# Mock the retrieve method
mock_client.tools.retrieve.return_value = github_tool
# Execute should raise ToolVersionRequiredError since version will be 'latest'
with pytest.raises(ToolVersionRequiredError) as exc_info:
tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
assert "Toolkit version not specified" in str(exc_info.value)
def test_execute_allows_latest_with_dangerously_skip_version_check(self):
"""Test that execute allows 'latest' version when dangerously_skip_version_check is True."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance without toolkit versions (defaults to latest)
tools = Tools(
client=mock_client,
provider=mock_provider,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute should succeed with dangerously_skip_version_check=True
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
dangerously_skip_version_check=True,
)
# Verify execution succeeded
assert result["successful"] is True
assert result["data"] == {"result": "success"}
# Verify the client was called with version="latest"
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == "latest"
def test_execute_with_specific_version_no_error(self):
"""Test that execute works with specific version without raising error."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance without toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with specific version should succeed
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
version="20251201_01",
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the specific version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == "20251201_01"
def test_execute_uses_instance_toolkit_versions(self):
"""Test that execute method uses instance-level toolkit versions."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions configuration
toolkit_versions = {"github": "20251201_01", "slack": "20251201_02"}
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute should use the configured toolkit version
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the configured version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == "20251201_01"
def test_execute_version_parameter_overrides_toolkit_versions(self):
"""Test that explicit version parameter overrides instance toolkit versions."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create toolkit versions configuration
toolkit_versions = {"github": "20251201_01"}
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions=toolkit_versions,
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with explicit version should override instance version
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
version="20251201_03", # Explicit version
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the explicit version, not instance version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == "20251201_03"
def test_execute_with_connected_account_id(self):
"""Test that execute passes connected_account_id correctly."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with connected_account_id
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
connected_account_id="test-account-123",
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the connected_account_id
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["connected_account_id"] == "test-account-123"
def test_execute_with_custom_auth_params(self):
"""Test that execute passes custom_auth_params correctly."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with custom_auth_params
custom_auth = {"api_key": "test-key"}
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
custom_auth_params=custom_auth,
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the custom_auth_params
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["custom_auth_params"] == custom_auth
def test_execute_with_user_id_and_text(self):
"""Test that execute passes user_id and text parameters correctly."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with user_id and text
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
user_id="user-123",
text="Additional context",
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with user_id and text
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["user_id"] == "user-123"
assert call_args.kwargs["text"] == "Additional context"
def test_execute_with_modifiers_before_execute(self):
"""Test that execute applies before_execute modifiers correctly."""
from composio.core.models._modifiers import before_execute
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Create a before_execute modifier that changes the arguments
def modify_arguments(tool, toolkit, params):
params["arguments"]["owner"] = "modified-owner"
return params
modifier = before_execute(modify_arguments)
# Execute with modifier
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
modifiers=[modifier],
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with modified arguments
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["arguments"]["owner"] == "modified-owner"
def test_execute_with_modifiers_after_execute(self):
"""Test that execute applies after_execute modifiers correctly."""
from composio.core.models._modifiers import after_execute
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Create an after_execute modifier that modifies the response
def modify_response(tool, toolkit, response):
response["data"]["modified"] = True
return response
modifier = after_execute(modify_response)
# Execute with modifier
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
modifiers=[modifier],
)
# Verify execution succeeded and response was modified
assert result["successful"] is True
assert result["data"]["modified"] is True
def test_merge_before_file_upload_scopes_by_tool(self):
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
scoped = before_file_upload(tools=["OTHER_TOOL"])(lambda p, t, k: p + "_X")
fn = merge_before_file_upload([scoped], tool="MY", toolkit="gh")
assert fn is not None
assert (
fn({"path": "/a", "source": "path", "tool": "MY", "toolkit": "gh"}) == "/a"
)
all_tools = before_file_upload(lambda p, t, k: p + "_Y")
fn2 = merge_before_file_upload([all_tools], tool="MY", toolkit="gh")
assert fn2 is not None
assert (
fn2({"path": "/a", "source": "path", "tool": "MY", "toolkit": "gh"})
== "/a_Y"
)
def test_merge_before_file_upload_chains_modifiers(self):
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
m1 = before_file_upload(lambda p, t, k: f"{p}|1")
m2 = before_file_upload(lambda p, t, k: f"{p}|2")
fn = merge_before_file_upload([m1, m2], tool="T", toolkit="k")
assert fn is not None
assert (
fn({"path": "p", "source": "path", "tool": "T", "toolkit": "k"}) == "p|1|2"
)
def test_before_file_upload_context_form_receives_source(self):
"""New-form hooks (single ``context`` arg) see ``source`` in the context."""
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
seen: t.Dict[str, t.Any] = {}
def hook(ctx):
seen.update(ctx)
return ctx["path"] + "!"
m = before_file_upload(hook)
fn = merge_before_file_upload([m], tool="T", toolkit="k")
assert fn is not None
out = fn({"path": "/x", "source": "path", "tool": "T", "toolkit": "k"})
assert out == "/x!"
assert seen == {"path": "/x", "source": "path", "tool": "T", "toolkit": "k"}
seen.clear()
out = fn(
{
"path": "https://example.com/a.pdf",
"source": "url",
"tool": "T",
"toolkit": "k",
}
)
assert out == "https://example.com/a.pdf!"
assert seen["source"] == "url"
def test_before_file_upload_legacy_3arg_form_still_works(self):
"""Hooks declared as ``(path, tool, toolkit)`` keep working unchanged."""
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
def legacy(path, tool, toolkit):
return f"{path}@{tool}/{toolkit}"
m = before_file_upload(legacy)
fn = merge_before_file_upload([m], tool="T", toolkit="k")
assert fn is not None
# Driver always passes context form; adapter unwraps to positional args.
assert (
fn({"path": "/x", "source": "path", "tool": "T", "toolkit": "k"})
== "/x@T/k"
)
def test_before_file_upload_chain_preserves_source(self):
"""``source`` stays stable as the ``path`` is rewritten down the chain."""
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
seen_sources: t.List[str] = []
def h1(ctx):
seen_sources.append(ctx["source"])
return ctx["path"] + "|1"
def h2(ctx):
seen_sources.append(ctx["source"])
return ctx["path"] + "|2"
fn = merge_before_file_upload(
[before_file_upload(h1), before_file_upload(h2)], tool="T", toolkit="k"
)
assert fn is not None
out = fn({"path": "p", "source": "url", "tool": "T", "toolkit": "k"})
assert out == "p|1|2"
assert seen_sources == ["url", "url"]
def test_before_file_upload_abort_propagates_as_false(self):
from composio.core.models._modifiers import (
before_file_upload,
merge_before_file_upload,
)
def abort(ctx):
return False
fn = merge_before_file_upload(
[before_file_upload(abort)], tool="T", toolkit="k"
)
assert fn is not None
assert (
fn({"path": "/x", "source": "path", "tool": "T", "toolkit": "k"}) is False
)
def test_execute_includes_before_file_upload_modifier_in_compose(
self,
):
"""``before_file_upload`` in ``modifiers`` is composed into substitute_file_uploads."""
from composio.core.models._modifiers import before_file_upload
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
dangerously_allow_auto_upload_download_files=True,
)
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
cap: t.Dict = {}
def capture_sub(**kwargs):
cap["bfu"] = kwargs.get("before_file_upload")
return kwargs["request"]
with patch.object(
tools._file_helper, "substitute_file_uploads", side_effect=capture_sub
):
mod = before_file_upload(lambda p, t, k: f"{p}>")
tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "a", "repo": "b"},
modifiers=[mod],
)
bfu = cap.get("bfu")
assert bfu is not None
assert (
bfu(
{
"path": "/p",
"source": "path",
"tool": "GITHUB_GET_REPOS",
"toolkit": "github",
}
)
== "/p>"
)
def test_execute_with_environment_variable_toolkit_version(self):
"""Test that execute uses environment variable for toolkit version."""
import os
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Set environment variable for github toolkit version
os.environ["COMPOSIO_TOOLKIT_VERSION_GITHUB"] = "20251201_08"
try:
# Create Tools instance with explicit toolkit versions that match the env var
# This simulates what happens when get_toolkit_version reads the env var
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_08"}, # This matches the env var
)
# Create a mock GitHub tool with proper structure
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
github_tool.input_parameters = {"type": "object", "properties": {}}
# Mock the retrieve method
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute should use the environment variable version
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with the env variable version
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["version"] == "20251201_08"
finally:
# Clean up environment variable
if "COMPOSIO_TOOLKIT_VERSION_GITHUB" in os.environ:
del os.environ["COMPOSIO_TOOLKIT_VERSION_GITHUB"]
def test_execute_raises_error_with_latest_via_env_variable(self):
"""Test that execute raises error when env variable sets version to 'latest'."""
import os
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Set environment variable for github toolkit version to 'latest'
os.environ["COMPOSIO_TOOLKIT_VERSION_GITHUB"] = "latest"
try:
# Create Tools instance without explicit toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
)
# Create a mock GitHub tool with proper structure
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
github_tool.input_parameters = {"type": "object", "properties": {}}
# Mock the retrieve method
mock_client.tools.retrieve.return_value = github_tool
# Execute should raise ToolVersionRequiredError since version is 'latest'
with pytest.raises(ToolVersionRequiredError):
tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
)
finally:
# Clean up environment variable
if "COMPOSIO_TOOLKIT_VERSION_GITHUB" in os.environ:
del os.environ["COMPOSIO_TOOLKIT_VERSION_GITHUB"]
def test_execute_with_custom_connection_data(self):
"""Test that execute passes custom_connection_data correctly."""
# Mock client and provider
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
# Create Tools instance with toolkit versions
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"github": "20251201_01"},
)
# Create a mock GitHub tool
github_tool = self.create_mock_tool("GITHUB_GET_REPOS", "github")
# Mock the get_raw_composio_tool_by_slug and retrieve methods
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=github_tool
):
mock_client.tools.retrieve.return_value = github_tool
# Mock the client's execute method
mock_execute_response = Mock()
mock_execute_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_execute_response
# Execute with custom_connection_data
custom_connection = {"token": "custom-token"}
result = tools.execute(
slug="GITHUB_GET_REPOS",
arguments={"owner": "test", "repo": "test"},
custom_connection_data=custom_connection,
)
# Verify execution succeeded
assert result["successful"] is True
# Verify the client was called with custom_connection_data
mock_client.tools.execute.assert_called_once()
call_args = mock_client.tools.execute.call_args
assert call_args.kwargs["custom_connection_data"] == custom_connection
class TestSerializeArguments:
"""Test _serialize_arguments and _needs_serialization helpers."""
def test_plain_dict_returns_same_object(self):
"""When no Pydantic models are present, return the original dict (no copy)."""
args = {"page_id": "abc-123", "title": "Hello"}
result = _serialize_arguments(args)
assert result is args
def test_needs_serialization_false_for_primitives(self):
assert not _needs_serialization({"s": "hello", "i": 42, "b": True})
def test_needs_serialization_true_for_basemodel(self):
class M(BaseModel):
x: int
assert _needs_serialization({"m": M(x=1)})
def test_basemodel_serialized(self):
class Block(BaseModel):
type: str
content: str
result = _serialize_arguments(
{"block": Block(type="paragraph", content="Hello")}
)
assert result == {"block": {"type": "paragraph", "content": "Hello"}}
def test_rootmodel_list_serialized(self):
class Block(BaseModel):
type: str
class BlockList(RootModel[list[Block]]):
pass
blocks = BlockList([Block(type="paragraph"), Block(type="heading_1")])
result = _serialize_arguments({"children": blocks})
assert result == {"children": [{"type": "paragraph"}, {"type": "heading_1"}]}
def test_list_of_models_serialized(self):
class Block(BaseModel):
type: str
result = _serialize_arguments(
{"children": [Block(type="paragraph"), Block(type="heading_1")]}
)
assert result == {"children": [{"type": "paragraph"}, {"type": "heading_1"}]}
def test_nested_dict_with_models(self):
class Inner(BaseModel):
value: int
result = _serialize_arguments({"outer": {"inner": Inner(value=42)}})
assert result == {"outer": {"inner": {"value": 42}}}
def test_execute_tool_serializes_pydantic_arguments(self):
"""Regression test for PLEN-1514: Pydantic models in arguments must be
serialized to plain dicts before being sent to the API."""
mock_client = mock_http_client()
mock_provider = Mock()
mock_provider.name = "test_provider"
tools = Tools(
client=mock_client,
provider=mock_provider,
toolkit_versions={"notion": "latest"},
)
notion_tool = Tool(
name="Test NOTION_REPLACE_PAGE_CONTENT",
slug="NOTION_REPLACE_PAGE_CONTENT",
description="Test tool",
input_parameters={},
output_parameters={},
available_versions=["v1.0.0"],
version="v1.0.0",
scopes=[],
toolkit=tool_list_response.ItemToolkit(
name="Notion", slug="notion", logo=""
),
deprecated=tool_list_response.ItemDeprecated(
available_versions=["v1.0.0"],
displayName="Test NOTION_REPLACE_PAGE_CONTENT",
version="v1.0.0",
toolkit=tool_list_response.ItemDeprecatedToolkit(logo=""),
is_deprecated=False,
),
is_deprecated=False,
no_auth=False,
tags=[],
)
class Block(BaseModel):
type: str
content: str
with patch.object(
tools, "get_raw_composio_tool_by_slug", return_value=notion_tool
):
mock_response = Mock()
mock_response.model_dump.return_value = {
"data": {"result": "success"},
"error": None,
"successful": True,
}
mock_client.tools.execute.return_value = mock_response
tools._execute_tool(
slug="NOTION_REPLACE_PAGE_CONTENT",
arguments={
"page_id": "abc-123",
"children": [
Block(type="paragraph", content="Hello from composio"),
],
},
dangerously_skip_version_check=True,
)
call_args = mock_client.tools.execute.call_args
sent_arguments = call_args.kwargs["arguments"]
assert sent_arguments == {
"page_id": "abc-123",
"children": [
{"type": "paragraph", "content": "Hello from composio"},
],
}
for child in sent_arguments["children"]:
assert not isinstance(child, BaseModel)