mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
7d36de8c0a
Construct a fresh OpenAIProvider per SDK instance instead of sharing a module-level singleton whose execute_tool binding was overwritten by the last-constructed instance, silently routing tool execution through the wrong client/API key. Regression test in tests/test_sdk.py. Fixes #4369 Co-authored-by: Adesh Deshmukh <adeshkd123@gmail.com> Claude-Session: https://claude.ai/code/session_015YPz5SzeScR9TkgoRi2p1F EOF -R ComposioHQ/composio
239 lines
11 KiB
Python
239 lines
11 KiB
Python
"""Test SDK functionality."""
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from composio import Composio, exceptions
|
|
from composio.core.provider._openai import OpenAIProvider
|
|
from composio.core.types import ToolkitVersionParam
|
|
|
|
|
|
class TestComposioSDK:
|
|
"""Test cases for Composio SDK."""
|
|
|
|
def test_sdk_requires_api_key(self):
|
|
"""Test that SDK requires an API key."""
|
|
with patch.dict(os.environ, {}, clear=True):
|
|
with pytest.raises(exceptions.ApiKeyNotProvidedError):
|
|
Composio()
|
|
|
|
def test_sdk_accepts_api_key_from_env(self):
|
|
"""Test that SDK accepts API key from environment."""
|
|
with patch.dict(os.environ, {"COMPOSIO_API_KEY": "test-key"}):
|
|
with patch("composio.core.models.Tools"):
|
|
with patch("composio.core.models.Toolkits"):
|
|
with patch("composio.core.models.Triggers"):
|
|
with patch("composio.core.models.AuthConfigs"):
|
|
with patch("composio.core.models.ConnectedAccounts"):
|
|
sdk = Composio()
|
|
assert sdk is not None
|
|
|
|
def test_sdk_accepts_api_key_as_parameter(self):
|
|
"""Test that SDK accepts API key as parameter."""
|
|
with patch("composio.core.models.Tools"):
|
|
with patch("composio.core.models.Toolkits"):
|
|
with patch("composio.core.models.Triggers"):
|
|
with patch("composio.core.models.AuthConfigs"):
|
|
with patch("composio.core.models.ConnectedAccounts"):
|
|
sdk = Composio(api_key="test-key")
|
|
assert sdk is not None
|
|
|
|
def test_sdk_config_types(self):
|
|
"""Test SDK configuration types."""
|
|
from composio.sdk import SDKConfig
|
|
|
|
# Test that SDKConfig is a TypedDict
|
|
assert hasattr(SDKConfig, "__annotations__")
|
|
|
|
# Test that all expected fields are present
|
|
expected_fields = {
|
|
"environment",
|
|
"api_key",
|
|
"base_url",
|
|
"timeout",
|
|
"max_retries",
|
|
"allow_tracking",
|
|
"file_download_dir",
|
|
"toolkit_versions",
|
|
"dangerously_allow_auto_upload_download_files",
|
|
"sensitive_file_upload_protection",
|
|
"file_upload_path_deny_segments",
|
|
"file_upload_dirs",
|
|
}
|
|
assert set(SDKConfig.__annotations__.keys()) == expected_fields
|
|
|
|
def test_toolkit_version_param_types(self):
|
|
"""Test toolkit version parameter types."""
|
|
from composio.core.types import (
|
|
ToolkitLatestVersion,
|
|
ToolkitVersion,
|
|
ToolkitVersions,
|
|
)
|
|
|
|
# Test that types are defined correctly
|
|
assert ToolkitLatestVersion is not None
|
|
assert ToolkitVersion is not None
|
|
assert ToolkitVersions is not None
|
|
assert ToolkitVersionParam is not None
|
|
|
|
def test_sdk_has_required_attributes(self):
|
|
"""Test that SDK has required attributes after initialization."""
|
|
with patch.dict(os.environ, {"COMPOSIO_API_KEY": "test-key"}):
|
|
with patch("composio.core.models.Tools"):
|
|
with patch("composio.core.models.Toolkits"):
|
|
with patch("composio.core.models.Triggers"):
|
|
with patch("composio.core.models.AuthConfigs"):
|
|
with patch("composio.core.models.ConnectedAccounts"):
|
|
sdk = Composio()
|
|
|
|
# Check that all required attributes are present
|
|
assert hasattr(sdk, "tools")
|
|
assert hasattr(sdk, "toolkits")
|
|
assert hasattr(sdk, "triggers")
|
|
assert hasattr(sdk, "auth_configs")
|
|
assert hasattr(sdk, "connected_accounts")
|
|
assert hasattr(sdk, "provider")
|
|
assert hasattr(sdk, "client")
|
|
|
|
def test_sdk_default_provider(self):
|
|
"""Test that SDK uses default provider."""
|
|
with patch.dict(os.environ, {"COMPOSIO_API_KEY": "test-key"}):
|
|
with patch("composio.core.models.Tools"):
|
|
with patch("composio.core.models.Toolkits"):
|
|
with patch("composio.core.models.Triggers"):
|
|
with patch("composio.core.models.AuthConfigs"):
|
|
with patch("composio.core.models.ConnectedAccounts"):
|
|
sdk = Composio()
|
|
|
|
# Check that provider is set
|
|
assert sdk.provider is not None
|
|
assert hasattr(sdk.provider, "name")
|
|
|
|
def test_default_provider_is_isolated_per_instance(self):
|
|
"""Regression test for #4369.
|
|
|
|
The default provider used to be a module-level singleton. Because every
|
|
``Tools`` instance rebinds ``provider.execute_tool`` to itself, the last
|
|
constructed ``Composio()`` silently took over tool execution for every
|
|
other instance, routing calls through the wrong API key.
|
|
"""
|
|
sdk_a = Composio(api_key="key-a")
|
|
sdk_b = Composio(api_key="key-b")
|
|
|
|
assert isinstance(sdk_a.provider, OpenAIProvider)
|
|
assert isinstance(sdk_b.provider, OpenAIProvider)
|
|
assert sdk_a.provider is not sdk_b.provider
|
|
|
|
# execute_tool is a functools.partial over Tools.execute; each provider
|
|
# must stay bound to the Tools of the instance that created it.
|
|
assert sdk_a.provider.execute_tool.func.__self__ is sdk_a.tools
|
|
assert sdk_b.provider.execute_tool.func.__self__ is sdk_b.tools
|
|
|
|
def test_explicit_provider_is_used_unchanged(self):
|
|
"""An explicitly passed provider instance is used as-is."""
|
|
provider = OpenAIProvider()
|
|
sdk = Composio(provider=provider, api_key="key-a")
|
|
assert sdk.provider is provider
|
|
|
|
def test_toolkit_versions_processing(self):
|
|
"""Test toolkit versions parameter processing."""
|
|
with patch.dict(os.environ, {"COMPOSIO_API_KEY": "test-key"}):
|
|
with patch("composio.core.models.Tools"):
|
|
with patch("composio.core.models.Toolkits"):
|
|
with patch("composio.core.models.Triggers"):
|
|
with patch("composio.core.models.AuthConfigs"):
|
|
with patch("composio.core.models.ConnectedAccounts"):
|
|
with patch(
|
|
"composio.sdk.get_toolkit_versions"
|
|
) as mock_get_versions:
|
|
mock_get_versions.return_value = "latest"
|
|
|
|
# Test with string version
|
|
Composio(toolkit_versions="v1.0.0")
|
|
mock_get_versions.assert_called_once()
|
|
|
|
# Reset mock for next test
|
|
mock_get_versions.reset_mock()
|
|
|
|
# Test with dict version
|
|
versions_dict = {
|
|
"github": "v1.0.0",
|
|
"slack": "latest",
|
|
}
|
|
Composio(toolkit_versions=versions_dict)
|
|
mock_get_versions.assert_called_once()
|
|
|
|
# Reset mock for next test
|
|
mock_get_versions.reset_mock()
|
|
|
|
# Test with None (default)
|
|
Composio()
|
|
mock_get_versions.assert_called_once()
|
|
|
|
def test_sdk_env_var_integration(self):
|
|
"""Test that SDK properly integrates with environment variables for toolkit versions."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"COMPOSIO_API_KEY": "test-key",
|
|
"COMPOSIO_TOOLKIT_VERSION_GITHUB": "v1.0.0",
|
|
"COMPOSIO_TOOLKIT_VERSION_SLACK": "v2.0.0",
|
|
},
|
|
):
|
|
with patch("composio.sdk.Tools") as mock_tools_class:
|
|
with patch("composio.sdk.Toolkits"):
|
|
with patch("composio.sdk.Triggers"):
|
|
with patch("composio.sdk.AuthConfigs"):
|
|
with patch("composio.sdk.ConnectedAccounts"):
|
|
# Create SDK instance without explicit toolkit versions
|
|
Composio()
|
|
|
|
# Verify that Tools was initialized with processed versions
|
|
mock_tools_class.assert_called_once()
|
|
call_args = mock_tools_class.call_args
|
|
|
|
# The toolkit_versions should be a dict from env vars
|
|
toolkit_versions = call_args.kwargs.get(
|
|
"toolkit_versions"
|
|
)
|
|
expected = {"github": "v1.0.0", "slack": "v2.0.0"}
|
|
assert toolkit_versions == expected
|
|
|
|
def test_sdk_user_override_env_vars(self):
|
|
"""Test that user-provided toolkit versions override environment variables."""
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"COMPOSIO_API_KEY": "test-key",
|
|
"COMPOSIO_TOOLKIT_VERSION_GITHUB": "env_version",
|
|
"COMPOSIO_TOOLKIT_VERSION_SLACK": "env_slack",
|
|
},
|
|
):
|
|
with patch("composio.sdk.Tools") as mock_tools_class:
|
|
with patch("composio.sdk.Toolkits"):
|
|
with patch("composio.sdk.Triggers"):
|
|
with patch("composio.sdk.AuthConfigs"):
|
|
with patch("composio.sdk.ConnectedAccounts"):
|
|
# User provides override
|
|
user_versions = {
|
|
"github": "user_override",
|
|
"jira": "user_jira",
|
|
}
|
|
Composio(toolkit_versions=user_versions)
|
|
|
|
# Verify Tools was initialized with merged versions
|
|
mock_tools_class.assert_called_once()
|
|
call_args = mock_tools_class.call_args
|
|
|
|
toolkit_versions = call_args.kwargs.get(
|
|
"toolkit_versions"
|
|
)
|
|
expected = {
|
|
"github": "user_override", # User override
|
|
"slack": "env_slack", # From env
|
|
"jira": "user_jira", # User provided
|
|
}
|
|
assert toolkit_versions == expected
|