mirror of
https://github.com/ComposioHQ/composio.git
synced 2026-09-22 11:46:35 +08:00
820abb9072
## Summary
Toolkit version maps are keyed by **normalized (lowercase) slugs** on
the *write* side — env vars (`COMPOSIO_TOOLKIT_VERSION_*`) and
user-supplied dicts both get their keys lowercased when the map is
built. But the *lookup* read the map with the **raw slug**. A pin
configured under a different casing (e.g. `{ "GitHub": "20250101_00" }`
or `COMPOSIO_TOOLKIT_VERSION_GITHUB` looked up as `GitHub`) therefore
missed the map and silently fell back to `"latest"`, which
`Tools.execute` then rejects with a version-required error — discarding
the user's explicit pin.
Rather than patch only the read side, this **centralizes the
normalization rule** into a single helper per SDK and routes **both**
the map-building (write) and lookup (read) paths through it, so the two
sides can never drift apart again:
- **Python** — `normalize_toolkit_slug()` in
`python/composio/utils/toolkit_version.py`
- **TypeScript** — `normalizeToolkitSlug()` in
`ts/packages/core/src/utils/toolkitVersion.ts`, imported by
`getToolkitVersionsFromEnv` in `sdk.ts`
Behavior is now identical across the two SDKs. This **supersedes
#3759**, which patched only the Python read side and left TS diverging.
## Backend verification
Confirmed against the backend (staging, **1403 toolkits across 15
pages**): every toolkit `slug` is already lowercase (the mixed-case
`name` field is display-only). So the *primary* execute path — which
resolves via `tool.toolkit.slug` — was never broken in practice. This
change:
- **hardens** the direct-util and mixed-case-config paths (a user
passing `{ "GitHub": "…" }` now resolves as expected), and
- **removes a latent cross-SDK divergence**: the TS suite previously
asserted `getToolkitVersion` was *case-sensitive* (`versions.test.ts`),
locking in the bug for an input the production code path can't actually
produce. That test is flipped to assert case-insensitive resolution.
## Tests
- **Python** (`python/tests/test_toolkit_version.py`): case-insensitive
lookup via `get_toolkit_versions` round-trip, and against a raw
pre-normalized map. `18 passed`.
- **TypeScript** (`ts/packages/core/test/core/versions.test.ts`):
replaced the case-sensitivity assertion with case-insensitive resolution
+ a write→read round-trip of a mixed-case user pin. Full core suite `997
passed`.
- Typecheck (`tsc --noEmit`) clean; `ruff` clean.
## Notes
- Changeset added: `@composio/core` patch.
- No public API change; the fix is behavioral (case-insensitive) plus an
internal shared helper.
188 lines
7.9 KiB
Python
188 lines
7.9 KiB
Python
"""Test toolkit version utilities."""
|
|
|
|
import pytest
|
|
|
|
from composio.core.types import ToolkitVersionParam
|
|
from composio.utils.toolkit_version import get_toolkit_version, get_toolkit_versions
|
|
|
|
|
|
class TestToolkitVersion:
|
|
"""Test cases for toolkit version utilities."""
|
|
|
|
def test_get_toolkit_version_with_string(self):
|
|
"""Test get_toolkit_version with string parameter."""
|
|
result = get_toolkit_version("github", "v1.0.0")
|
|
assert result == "v1.0.0"
|
|
|
|
def test_get_toolkit_version_with_dict(self):
|
|
"""Test get_toolkit_version with dict parameter."""
|
|
versions = {"github": "v1.0.0", "slack": "v2.0.0"}
|
|
result = get_toolkit_version("github", versions)
|
|
assert result == "v1.0.0"
|
|
|
|
def test_get_toolkit_version_with_dict_missing_key(self):
|
|
"""Test get_toolkit_version with dict parameter missing key."""
|
|
versions = {"slack": "v2.0.0"}
|
|
result = get_toolkit_version("github", versions)
|
|
assert result == "latest"
|
|
|
|
def test_get_toolkit_version_with_none(self):
|
|
"""Test get_toolkit_version with None parameter."""
|
|
result = get_toolkit_version("github", None)
|
|
assert result == "latest"
|
|
|
|
def test_get_toolkit_version_with_env_var(self):
|
|
"""Test get_toolkit_version with environment variable."""
|
|
with pytest.MonkeyPatch().context():
|
|
# The function doesn't directly use COMPOSIO_TOOLKIT_VERSION env var
|
|
# It only looks at the toolkit_versions parameter
|
|
result = get_toolkit_version("github", None)
|
|
assert result == "latest"
|
|
|
|
def test_get_toolkit_versions_with_string(self):
|
|
"""Test get_toolkit_versions with string parameter."""
|
|
result = get_toolkit_versions("v1.0.0")
|
|
assert result == "v1.0.0"
|
|
|
|
def test_get_toolkit_versions_with_dict(self):
|
|
"""Test get_toolkit_versions with dict parameter."""
|
|
versions = {"github": "v1.0.0", "slack": "v2.0.0"}
|
|
result = get_toolkit_versions(versions)
|
|
assert result == versions
|
|
|
|
def test_get_toolkit_versions_with_none(self):
|
|
"""Test get_toolkit_versions with None parameter."""
|
|
result = get_toolkit_versions(None)
|
|
assert result == "latest"
|
|
|
|
def test_get_toolkit_versions_with_env_toolkit_specific(self):
|
|
"""Test get_toolkit_versions with toolkit-specific environment variables."""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "v2.0.0")
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_SLACK", "v1.5.0")
|
|
result = get_toolkit_versions(None)
|
|
expected = {"github": "v2.0.0", "slack": "v1.5.0"}
|
|
assert result == expected
|
|
|
|
def test_get_toolkit_versions_env_and_user_override(self):
|
|
"""Test that user-provided versions override environment variables."""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
# Set environment variables
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "v1.0.0")
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_SLACK", "v2.0.0")
|
|
|
|
# User overrides should take precedence
|
|
user_versions = {"github": "v3.0.0", "jira": "v4.0.0"}
|
|
result = get_toolkit_versions(user_versions)
|
|
|
|
expected = {
|
|
"github": "v3.0.0", # User override
|
|
"slack": "v2.0.0", # From env
|
|
"jira": "v4.0.0", # User provided
|
|
}
|
|
assert result == expected
|
|
|
|
def test_get_toolkit_version_with_env_specific_toolkit(self):
|
|
"""Test get_toolkit_version works with environment-configured versions."""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "v2.5.0")
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_SLACK", "v1.8.0")
|
|
|
|
# Get versions from environment
|
|
env_versions = get_toolkit_versions(None)
|
|
|
|
# Test specific toolkit version retrieval
|
|
github_version = get_toolkit_version("github", env_versions)
|
|
slack_version = get_toolkit_version("slack", env_versions)
|
|
unknown_version = get_toolkit_version("unknown", env_versions)
|
|
|
|
assert github_version == "v2.5.0"
|
|
assert slack_version == "v1.8.0"
|
|
assert unknown_version == "latest"
|
|
|
|
def test_mixed_case_env_vars_normalized(self):
|
|
"""Test that mixed case environment variable names are normalized."""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
# Environment variables are typically uppercase
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "v1.0.0")
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_OPENAI", "v2.0.0")
|
|
|
|
result = get_toolkit_versions(None)
|
|
|
|
# Should be normalized to lowercase
|
|
assert "github" in result
|
|
assert "openai" in result
|
|
assert result["github"] == "v1.0.0"
|
|
assert result["openai"] == "v2.0.0"
|
|
|
|
def test_user_dict_case_normalization(self):
|
|
"""Test that user-provided dictionary keys are normalized to lowercase."""
|
|
user_versions = {"GitHub": "v1.0.0", "SLACK": "v2.0.0", "OpenAI": "v3.0.0"}
|
|
|
|
result = get_toolkit_versions(user_versions)
|
|
|
|
expected = {"github": "v1.0.0", "slack": "v2.0.0", "openai": "v3.0.0"}
|
|
assert result == expected
|
|
|
|
def test_get_toolkit_version_lookup_is_case_insensitive(self):
|
|
"""The lookup slug is matched case-insensitively.
|
|
|
|
Version maps are keyed by normalized (lowercase) slugs (write side
|
|
covered by test_user_dict_case_normalization), so the read side must
|
|
resolve any-cased slugs instead of silently returning 'latest'.
|
|
"""
|
|
versions = {"github": "v1.0.0"}
|
|
|
|
assert get_toolkit_version("GITHUB", versions) == "v1.0.0"
|
|
assert get_toolkit_version("GitHub", versions) == "v1.0.0"
|
|
assert get_toolkit_version("github", versions) == "v1.0.0"
|
|
|
|
def test_priority_order_matches_typescript(self):
|
|
"""Test that priority order matches TypeScript implementation.
|
|
|
|
Priority order should be:
|
|
1. String global version (overrides everything)
|
|
2. User-provided dict overrides env vars
|
|
3. Environment variables
|
|
4. Fallback to 'latest'
|
|
"""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
# Test 1: String global version overrides everything
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "env_version")
|
|
result = get_toolkit_version("github", "global_version")
|
|
assert result == "global_version"
|
|
|
|
# Test 2: User dict overrides env vars
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "env_version")
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_SLACK", "env_slack")
|
|
user_dict = {"github": "user_override"}
|
|
result = get_toolkit_versions(user_dict)
|
|
expected = {
|
|
"github": "user_override", # User override
|
|
"slack": "env_slack", # From env
|
|
}
|
|
assert result == expected
|
|
|
|
# Test 3: Environment variables when no user input
|
|
result = get_toolkit_versions(None)
|
|
expected = {"github": "env_version", "slack": "env_slack"}
|
|
assert result == expected
|
|
|
|
def test_empty_user_dict_uses_env_vars(self):
|
|
"""Test that empty user dict still uses environment variables."""
|
|
with pytest.MonkeyPatch().context() as m:
|
|
m.setenv("COMPOSIO_TOOLKIT_VERSION_GITHUB", "v1.0.0")
|
|
|
|
# Empty dict should still use env vars
|
|
result = get_toolkit_versions({})
|
|
expected = {"github": "v1.0.0"}
|
|
assert result == expected
|
|
|
|
def test_toolkit_version_param_type_annotation(self):
|
|
"""Test ToolkitVersionParam type annotation."""
|
|
import typing
|
|
|
|
# Test that ToolkitVersionParam is a Union type
|
|
assert hasattr(ToolkitVersionParam, "__origin__")
|
|
assert ToolkitVersionParam.__origin__ is typing.Union
|