mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-07 19:48:13 +00:00
* Exporting tool's metadata to AMP - initial work * Fix payload (nest under `tools` key) * Remove debug message + code simplification * Priting out detected tools * Extract module name * fix: address PR review feedback for tool metadata extraction - Use sha256 instead of md5 for module name hashing (lint S324) - Filter required list to match filtered properties in JSON schema * fix: Use sha256 instead of md5 for module name hashing (lint S324) - Add missing mocks to metadata extraction failure test * style: fix ruff formatting * fix: resolve mypy type errors in utils.py * fix: address bot review feedback on tool metadata - Use `is not None` instead of truthiness check so empty tools list is sent to the API rather than being silently dropped as None - Strip __init__ suffix from module path for tools in __init__.py files - Extend _unwrap_schema to handle function-before, function-wrap, and definitions wrapper types * fix: capture env_vars declared with Field(default_factory=...) When env_vars uses default_factory, pydantic stores a callable in the schema instead of a static default value. Fall back to calling the factory when no static default is present. --------- Co-authored-by: Greyson LaLonde <greyson.r.lalonde@gmail.com>
413 lines
15 KiB
Python
413 lines
15 KiB
Python
import os
|
|
import unittest
|
|
from unittest.mock import ANY, AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from crewai.cli.plus_api import PlusAPI
|
|
|
|
|
|
class TestPlusAPI(unittest.TestCase):
|
|
def setUp(self):
|
|
self.api_key = "test_api_key"
|
|
self.api = PlusAPI(self.api_key)
|
|
self.org_uuid = "test-org-uuid"
|
|
|
|
def test_init(self):
|
|
self.assertEqual(self.api.api_key, self.api_key)
|
|
self.assertEqual(self.api.headers["Authorization"], f"Bearer {self.api_key}")
|
|
self.assertEqual(self.api.headers["Content-Type"], "application/json")
|
|
self.assertTrue("CrewAI-CLI/" in self.api.headers["User-Agent"])
|
|
self.assertTrue(self.api.headers["X-Crewai-Version"])
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_login_to_tool_repository(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.login_to_tool_repository()
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/tools/login", json={}
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_login_to_tool_repository_with_user_identifier(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.login_to_tool_repository(user_identifier="test-hash-123")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/tools/login", json={"user_identifier": "test-hash-123"}
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
def assert_request_with_org_id(
|
|
self, mock_client_instance, method: str, endpoint: str, **kwargs
|
|
):
|
|
mock_client_instance.request.assert_called_once_with(
|
|
method,
|
|
f"{os.getenv('CREWAI_PLUS_URL')}{endpoint}",
|
|
headers={
|
|
"Authorization": ANY,
|
|
"Content-Type": ANY,
|
|
"User-Agent": ANY,
|
|
"X-Crewai-Version": ANY,
|
|
"X-Crewai-Organization-Id": self.org_uuid,
|
|
},
|
|
**kwargs,
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.Settings")
|
|
@patch("crewai.cli.plus_api.httpx.Client")
|
|
def test_login_to_tool_repository_with_org_uuid(
|
|
self, mock_client_class, mock_settings_class
|
|
):
|
|
mock_settings = MagicMock()
|
|
mock_settings.org_uuid = self.org_uuid
|
|
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
|
mock_settings_class.return_value = mock_settings
|
|
self.api = PlusAPI(self.api_key)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_client_instance.request.return_value = mock_response
|
|
mock_client_class.return_value.__enter__.return_value = mock_client_instance
|
|
|
|
response = self.api.login_to_tool_repository()
|
|
|
|
self.assert_request_with_org_id(
|
|
mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login", json={}
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_get_tool(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.get_tool("test_tool_handle")
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/tools/test_tool_handle"
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.Settings")
|
|
@patch("crewai.cli.plus_api.httpx.Client")
|
|
def test_get_tool_with_org_uuid(self, mock_client_class, mock_settings_class):
|
|
mock_settings = MagicMock()
|
|
mock_settings.org_uuid = self.org_uuid
|
|
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
|
mock_settings_class.return_value = mock_settings
|
|
self.api = PlusAPI(self.api_key)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_client_instance.request.return_value = mock_response
|
|
mock_client_class.return_value.__enter__.return_value = mock_client_instance
|
|
|
|
response = self.api.get_tool("test_tool_handle")
|
|
|
|
self.assert_request_with_org_id(
|
|
mock_client_instance, "GET", "/crewai_plus/api/v1/tools/test_tool_handle"
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_publish_tool(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
handle = "test_tool_handle"
|
|
public = True
|
|
version = "1.0.0"
|
|
description = "Test tool description"
|
|
encoded_file = "encoded_test_file"
|
|
|
|
response = self.api.publish_tool(
|
|
handle, public, version, description, encoded_file
|
|
)
|
|
|
|
params = {
|
|
"handle": handle,
|
|
"public": public,
|
|
"version": version,
|
|
"file": encoded_file,
|
|
"description": description,
|
|
"available_exports": None,
|
|
"tools_metadata": None,
|
|
}
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/tools", json=params
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.Settings")
|
|
@patch("crewai.cli.plus_api.httpx.Client")
|
|
def test_publish_tool_with_org_uuid(self, mock_client_class, mock_settings_class):
|
|
mock_settings = MagicMock()
|
|
mock_settings.org_uuid = self.org_uuid
|
|
mock_settings.enterprise_base_url = os.getenv('CREWAI_PLUS_URL')
|
|
mock_settings_class.return_value = mock_settings
|
|
self.api = PlusAPI(self.api_key)
|
|
|
|
mock_client_instance = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_client_instance.request.return_value = mock_response
|
|
mock_client_class.return_value.__enter__.return_value = mock_client_instance
|
|
|
|
handle = "test_tool_handle"
|
|
public = True
|
|
version = "1.0.0"
|
|
description = "Test tool description"
|
|
encoded_file = "encoded_test_file"
|
|
|
|
response = self.api.publish_tool(
|
|
handle, public, version, description, encoded_file
|
|
)
|
|
|
|
expected_params = {
|
|
"handle": handle,
|
|
"public": public,
|
|
"version": version,
|
|
"file": encoded_file,
|
|
"description": description,
|
|
"available_exports": None,
|
|
"tools_metadata": None,
|
|
}
|
|
|
|
self.assert_request_with_org_id(
|
|
mock_client_instance, "POST", "/crewai_plus/api/v1/tools", json=expected_params
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_publish_tool_without_description(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
handle = "test_tool_handle"
|
|
public = False
|
|
version = "2.0.0"
|
|
description = None
|
|
encoded_file = "encoded_test_file"
|
|
|
|
response = self.api.publish_tool(
|
|
handle, public, version, description, encoded_file
|
|
)
|
|
|
|
params = {
|
|
"handle": handle,
|
|
"public": public,
|
|
"version": version,
|
|
"file": encoded_file,
|
|
"description": description,
|
|
"available_exports": None,
|
|
"tools_metadata": None,
|
|
}
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/tools", json=params
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_publish_tool_with_tools_metadata(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
handle = "test_tool_handle"
|
|
public = True
|
|
version = "1.0.0"
|
|
description = "Test tool description"
|
|
encoded_file = "encoded_test_file"
|
|
available_exports = [{"name": "MyTool"}]
|
|
tools_metadata = [
|
|
{
|
|
"name": "MyTool",
|
|
"humanized_name": "my_tool",
|
|
"description": "A test tool",
|
|
"run_params_schema": {"type": "object", "properties": {}},
|
|
"init_params_schema": {"type": "object", "properties": {}},
|
|
"env_vars": [{"name": "API_KEY", "description": "API key", "required": True, "default": None}],
|
|
}
|
|
]
|
|
|
|
response = self.api.publish_tool(
|
|
handle, public, version, description, encoded_file,
|
|
available_exports=available_exports,
|
|
tools_metadata=tools_metadata,
|
|
)
|
|
|
|
params = {
|
|
"handle": handle,
|
|
"public": public,
|
|
"version": version,
|
|
"file": encoded_file,
|
|
"description": description,
|
|
"available_exports": available_exports,
|
|
"tools_metadata": {"package": handle, "tools": tools_metadata},
|
|
}
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/tools", json=params
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.httpx.Client")
|
|
def test_make_request(self, mock_client_class):
|
|
mock_client_instance = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_client_instance.request.return_value = mock_response
|
|
mock_client_class.return_value.__enter__.return_value = mock_client_instance
|
|
|
|
response = self.api._make_request("GET", "test_endpoint")
|
|
|
|
mock_client_class.assert_called_once_with(trust_env=False, verify=True)
|
|
mock_client_instance.request.assert_called_once_with(
|
|
"GET", f"{self.api.base_url}/test_endpoint", headers=self.api.headers
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_deploy_by_name(self, mock_make_request):
|
|
self.api.deploy_by_name("test_project")
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/crews/by-name/test_project/deploy"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_deploy_by_uuid(self, mock_make_request):
|
|
self.api.deploy_by_uuid("test_uuid")
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/crews/test_uuid/deploy"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_crew_status_by_name(self, mock_make_request):
|
|
self.api.crew_status_by_name("test_project")
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/crews/by-name/test_project/status"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_crew_status_by_uuid(self, mock_make_request):
|
|
self.api.crew_status_by_uuid("test_uuid")
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/crews/test_uuid/status"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_crew_by_name(self, mock_make_request):
|
|
self.api.crew_by_name("test_project")
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/crews/by-name/test_project/logs/deployment"
|
|
)
|
|
|
|
self.api.crew_by_name("test_project", "custom_log")
|
|
mock_make_request.assert_called_with(
|
|
"GET", "/crewai_plus/api/v1/crews/by-name/test_project/logs/custom_log"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_crew_by_uuid(self, mock_make_request):
|
|
self.api.crew_by_uuid("test_uuid")
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/crews/test_uuid/logs/deployment"
|
|
)
|
|
|
|
self.api.crew_by_uuid("test_uuid", "custom_log")
|
|
mock_make_request.assert_called_with(
|
|
"GET", "/crewai_plus/api/v1/crews/test_uuid/logs/custom_log"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_delete_crew_by_name(self, mock_make_request):
|
|
self.api.delete_crew_by_name("test_project")
|
|
mock_make_request.assert_called_once_with(
|
|
"DELETE", "/crewai_plus/api/v1/crews/by-name/test_project"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_delete_crew_by_uuid(self, mock_make_request):
|
|
self.api.delete_crew_by_uuid("test_uuid")
|
|
mock_make_request.assert_called_once_with(
|
|
"DELETE", "/crewai_plus/api/v1/crews/test_uuid"
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_list_crews(self, mock_make_request):
|
|
self.api.list_crews()
|
|
mock_make_request.assert_called_once_with("GET", "/crewai_plus/api/v1/crews")
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
|
def test_create_crew(self, mock_make_request):
|
|
payload = {"name": "test_crew"}
|
|
self.api.create_crew(payload)
|
|
mock_make_request.assert_called_once_with(
|
|
"POST", "/crewai_plus/api/v1/crews", json=payload
|
|
)
|
|
|
|
@patch("crewai.cli.plus_api.Settings")
|
|
@patch.dict(os.environ, {"CREWAI_PLUS_URL": ""})
|
|
def test_custom_base_url(self, mock_settings_class):
|
|
mock_settings = MagicMock()
|
|
mock_settings.enterprise_base_url = "https://custom-url.com/api"
|
|
mock_settings_class.return_value = mock_settings
|
|
custom_api = PlusAPI("test_key")
|
|
self.assertEqual(
|
|
custom_api.base_url,
|
|
"https://custom-url.com/api",
|
|
)
|
|
|
|
@patch.dict(os.environ, {"CREWAI_PLUS_URL": "https://custom-url-from-env.com"})
|
|
def test_custom_base_url_from_env(self):
|
|
custom_api = PlusAPI("test_key")
|
|
self.assertEqual(
|
|
custom_api.base_url,
|
|
"https://custom-url-from-env.com",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("httpx.AsyncClient")
|
|
async def test_get_agent(mock_async_client_class):
|
|
api = PlusAPI("test_api_key")
|
|
mock_response = MagicMock()
|
|
mock_client_instance = AsyncMock()
|
|
mock_client_instance.get.return_value = mock_response
|
|
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
|
|
|
response = await api.get_agent("test_agent_handle")
|
|
|
|
mock_client_instance.get.assert_called_once_with(
|
|
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
|
headers=api.headers,
|
|
)
|
|
assert response == mock_response
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("httpx.AsyncClient")
|
|
@patch("crewai.cli.plus_api.Settings")
|
|
async def test_get_agent_with_org_uuid(mock_settings_class, mock_async_client_class):
|
|
org_uuid = "test-org-uuid"
|
|
mock_settings = MagicMock()
|
|
mock_settings.org_uuid = org_uuid
|
|
mock_settings.enterprise_base_url = os.getenv("CREWAI_PLUS_URL")
|
|
mock_settings_class.return_value = mock_settings
|
|
|
|
api = PlusAPI("test_api_key")
|
|
|
|
mock_response = MagicMock()
|
|
mock_client_instance = AsyncMock()
|
|
mock_client_instance.get.return_value = mock_response
|
|
mock_async_client_class.return_value.__aenter__.return_value = mock_client_instance
|
|
|
|
response = await api.get_agent("test_agent_handle")
|
|
|
|
mock_client_instance.get.assert_called_once_with(
|
|
f"{api.base_url}/crewai_plus/api/v1/agents/test_agent_handle",
|
|
headers=api.headers,
|
|
)
|
|
assert "X-Crewai-Organization-Id" in api.headers
|
|
assert api.headers["X-Crewai-Organization-Id"] == org_uuid
|
|
assert response == mock_response
|