mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-10-22 18:03:10 +02:00
* fix(core): send AMP requests through the proxy the environment names PlusAPI built its httpx clients with trust_env=False, so HTTP(S)_PROXY, NO_PROXY and SSL_CERT_FILE were ignored and every AMP call tried a direct connection. On a corporate network or inside a coding-agent sandbox (Claude Code, Codex), which only allow traffic through that proxy, deploy, eval, org, triggers, trace upload, skills and repository agents all failed with "[Errno 8] nodename nor servname provided", while `crewai login` (module-level httpx.post) worked. trust_env=False dates from #1455, when the client used requests, where it also stopped ~/.netrc credentials from replacing the AMP bearer token. httpx 0.28 no longer reads .netrc, so the flag now only disables proxy and CA-bundle variables. Remove it from the three clients (core request and multipart, and the CLI's multipart override). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(core): keep the direct-request proxy test off the OS proxy settings With no proxy variables set, httpx falls back to urllib's getproxies(), which reads the macOS or Windows system proxy settings. On a machine with a system proxy configured, the "no proxy variables" test would have routed through it. Model a machine with none configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): keep a project's .env from choosing how crewai eval sends the login crewai eval already refuses to send the saved login to an AMP host that only the project's .env names. With AMP requests now honoring proxy and CA variables, the same .env could still pick the route: its own proxy and its own CA bundle, past that check. eval and eval --models now leave *_proxy, SSL_CERT_FILE and SSL_CERT_DIR out when they load the project's .env, say so, and keep the machine's own settings (shell exports, OS proxy). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): take AMP's proxy and CA from the machine, not a project's .env AMP requests carry the saved login. Honoring HTTP(S)_PROXY and SSL_CERT_FILE from the live environment let a cloned project's .env choose both: `crewai run` on a JSON crew or a declarative flow loads it with override=True, and `crewai.llm` loads it again on import, which also undid the filter `crewai eval` applied to its own loader. PlusAPI now pins the proxy and CA variables when crewai_core.plus_api is first imported, which the CLI does at startup, and hands them to httpx explicitly with trust_env=False. With none set, the OS proxy settings apply, read live. The eval loader goes back to main's, since the client now covers every command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): pin AMP's proxy and CA in the JSON-crew runner too `crewai run` runs a JSON crew in a fresh `uv run python -c` interpreter that never imports cli.py, and `_run_json_crew` loads the project's .env with override=True before tracing first imports crewai_core.plus_api, so the pin recorded the project's proxy and CA. The runner now imports plus_api before anything else. Declarative flows re-run through `uv run crewai run`, which already pins in cli.py. Also keeps `_requests_sent_by` off machine and OS proxies, so a developer's proxy can't mount a real transport ahead of its MockTransport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): keep urllib's CGI proxy rule, and surface a broken plus_api import The pinned proxy map dropped urllib's REQUEST_METHOD rule: in a CGI process an uppercase HTTP_PROXY can come from a client's Proxy header, and AMP's login and deployment ZIPs would follow it. REQUEST_METHOD is now pinned with the other transport variables, and the map is read exactly as urllib reads it. The JSON-crew runner skipped any ImportError from crewai_core.plus_api. It now skips only a crewai-core too old to have the module; any other failure stops the run with its own error instead of running with nothing pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): import the proxy pin with the project folder off sys.path `python -c` puts the working directory, the project, first on sys.path, so a project folder named crewai_core could supply the module that pins where the saved login travels. The JSON-crew runner now imports the pin with the project off the path and restores the path right after, so project code (custom tools importing their neighbours) still finds its own modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): pin AMP's proxy and CA first thing in `import crewai` A crew run in its own process (`uv run run_crew`) never imports the CLI, so `import crewai` is where the pin happens there. It already preceded the project's .env, loaded by crewai.llm on import, but only by import order. The pin is now the first import in crewai/__init__.py, and a test runs a fresh interpreter in a project whose .env names a proxy and CA: the .env loads, and the pin still holds only what the machine set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): keep the machine's proxy and CA for the crew `crewai eval` runs `crewai eval` loads the project's .env into its own process and then runs the crew in a child that inherits that environment. The child pins what it inherits, so a .env naming HTTPS_PROXY and SSL_CERT_FILE became the run's machine transport, and that run's traces carry the saved login. The proxy and CA variables go back to the machine's pin right after the .env loads; the rest of the .env still applies, and the run loads the .env itself after its own pin, as under `crewai run`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(core): keep a project's UV_ENV_FILE out of the crew `crewai eval` runs `crewai eval` loads the project's .env and starts the crew with `uv run`. A .env naming itself as UV_ENV_FILE had uv load it into the run before Python started, so the run pinned the project's proxy and CA anyway. UV_ENV_FILE now counts among the variables restored to the machine's. The pin test starts the run through `uv run`, as the CLI does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
673 lines
25 KiB
Python
673 lines
25 KiB
Python
from collections.abc import Callable
|
|
import os
|
|
import unittest
|
|
from unittest.mock import ANY, MagicMock, patch
|
|
|
|
from crewai_cli.plus_api import PlusAPI
|
|
import httpx
|
|
import pytest
|
|
|
|
|
|
_RealHttpxClient = httpx.Client
|
|
|
|
|
|
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.assertIn("CrewAI-CLI/", self.api.headers["User-Agent"])
|
|
self.assertTrue(self.api.headers["X-Crewai-Version"])
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_create_evaluation(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.create_evaluation("6f31fe1a-20bd-4bfe-a011-25d6b9341f62")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/tracing/evaluations",
|
|
json={"execution_id": "6f31fe1a-20bd-4bfe-a011-25d6b9341f62"},
|
|
timeout=120.0, # AMP reads the run's spans inside this request
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_create_evaluation_files_it_under_the_project(self, mock_make_request):
|
|
self.api.create_evaluation("run-1", eval_config="{}", project_id="proj-1")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/tracing/evaluations",
|
|
json={"execution_id": "run-1", "eval_config": "{}", "project_id": "proj-1"},
|
|
timeout=120.0,
|
|
)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_create_models_evaluation(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.create_models_evaluation(
|
|
["openai/gpt-4o-mini", "anthropic/claude-haiku-4-5"],
|
|
project_id="proj-1",
|
|
eval_config="// ours\n{}",
|
|
deployment_id="dep-1",
|
|
)
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/tracing/evaluations",
|
|
json={
|
|
"kind": "models",
|
|
"project_id": "proj-1",
|
|
"models": ["openai/gpt-4o-mini", "anthropic/claude-haiku-4-5"],
|
|
"eval_config": "// ours\n{}",
|
|
"deployment_id": "dep-1",
|
|
},
|
|
timeout=120.0,
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_create_models_evaluation_sends_only_what_it_was_given(
|
|
self, mock_make_request
|
|
):
|
|
self.api.create_models_evaluation(["openai/gpt-4o-mini"], project_id="proj-1")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/tracing/evaluations",
|
|
json={
|
|
"kind": "models",
|
|
"project_id": "proj-1",
|
|
"models": ["openai/gpt-4o-mini"],
|
|
},
|
|
timeout=120.0,
|
|
)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_get_evaluation(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.get_evaluation("ev-1")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET",
|
|
"/crewai_plus/api/v1/tracing/evaluations/ev-1",
|
|
params={"can_export": "1"},
|
|
timeout=30.0,
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_export_evaluation(self, mock_make_request):
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = self.api.export_evaluation("ev-1")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET",
|
|
"/crewai_plus/api/v1/tracing/evaluations/ev-1/export",
|
|
timeout=httpx.Timeout(30.0, read=120.0),
|
|
)
|
|
self.assertEqual(response, mock_response)
|
|
|
|
@patch("crewai_core.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)
|
|
|
|
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_core.plus_api.Settings")
|
|
@patch("crewai_core.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_core.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_core.plus_api.Settings")
|
|
@patch("crewai_core.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_core.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_core.plus_api.Settings")
|
|
@patch("crewai_core.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_core.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.dict("crewai_core.plus_api._MACHINE_TRANSPORT_ENV", clear=True)
|
|
@patch("crewai_core.plus_api._os_proxies", dict)
|
|
@patch("crewai_core.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(
|
|
verify=True, trust_env=False, mounts={}
|
|
)
|
|
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_core.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_core.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_core.plus_api.PlusAPI._make_request")
|
|
def test_a_forced_push_says_so_and_an_unforced_one_sends_what_it_always_did(self, mock_make_request):
|
|
self.api.deploy_by_uuid("test_uuid", force=True)
|
|
mock_make_request.assert_called_with(
|
|
"POST", "/crewai_plus/api/v1/crews/test_uuid/deploy", json={"force": True}
|
|
)
|
|
self.api.deploy_by_name("test_project", project_id="proj-1", force=True)
|
|
mock_make_request.assert_called_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/by-name/test_project/deploy",
|
|
json={"project_id": "proj-1", "force": True},
|
|
)
|
|
self.api.deploy_by_uuid("test_uuid", force=False)
|
|
mock_make_request.assert_called_with(
|
|
"POST", "/crewai_plus/api/v1/crews/test_uuid/deploy"
|
|
)
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_deploy_sends_the_project_id_when_there_is_one(self, mock_make_request):
|
|
self.api.deploy_by_name("test_project", project_id="proj-1")
|
|
mock_make_request.assert_called_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/by-name/test_project/deploy",
|
|
json={"project_id": "proj-1"},
|
|
)
|
|
self.api.deploy_by_uuid("test_uuid", project_id="proj-1")
|
|
mock_make_request.assert_called_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/test_uuid/deploy",
|
|
json={"project_id": "proj-1"},
|
|
)
|
|
|
|
@patch("crewai_cli.plus_api.PlusAPI._make_multipart_request")
|
|
def test_zip_deploys_send_the_project_id_when_there_is_one(
|
|
self, mock_make_multipart_request
|
|
):
|
|
self.api.create_crew_from_zip("/tmp/test.zip", name="c", project_id="proj-1")
|
|
mock_make_multipart_request.assert_called_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/zip",
|
|
zip_file_path="/tmp/test.zip",
|
|
data={"name": "c", "project_id": "proj-1"},
|
|
timeout=300,
|
|
)
|
|
self.api.update_crew_from_zip("u", "/tmp/test.zip", project_id="proj-1")
|
|
mock_make_multipart_request.assert_called_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/u/zip_update",
|
|
zip_file_path="/tmp/test.zip",
|
|
data={"project_id": "proj-1"},
|
|
timeout=300,
|
|
)
|
|
|
|
@patch("crewai_core.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_core.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_core.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_core.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_core.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_core.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_core.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_core.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.PlusAPI._make_multipart_request")
|
|
def test_create_crew_from_zip(self, mock_make_multipart_request):
|
|
self.api.create_crew_from_zip(
|
|
"/tmp/test.zip",
|
|
name="test_crew",
|
|
env={"ENV_VAR": "value"},
|
|
)
|
|
mock_make_multipart_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/zip",
|
|
zip_file_path="/tmp/test.zip",
|
|
data={"name": "test_crew", "env[ENV_VAR]": "value"},
|
|
timeout=300,
|
|
)
|
|
|
|
@patch("crewai_cli.plus_api.PlusAPI._make_multipart_request")
|
|
def test_update_crew_from_zip(self, mock_make_multipart_request):
|
|
self.api.update_crew_from_zip(
|
|
"test_uuid",
|
|
"/tmp/test.zip",
|
|
env={"ENV_VAR": "value"},
|
|
)
|
|
mock_make_multipart_request.assert_called_once_with(
|
|
"POST",
|
|
"/crewai_plus/api/v1/crews/test_uuid/zip_update",
|
|
zip_file_path="/tmp/test.zip",
|
|
data={"env[ENV_VAR]": "value"},
|
|
timeout=300,
|
|
)
|
|
self.api.update_crew_from_zip("test_uuid", "/tmp/test.zip", force=True)
|
|
assert mock_make_multipart_request.call_args.kwargs["data"] == {"force": "true"}
|
|
|
|
@patch("crewai_core.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",
|
|
)
|
|
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
def test_get_agent(mock_make_request):
|
|
api = PlusAPI("test_api_key")
|
|
mock_response = MagicMock()
|
|
mock_make_request.return_value = mock_response
|
|
|
|
response = api.get_agent("test_agent_handle")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
|
)
|
|
assert response == mock_response
|
|
|
|
|
|
@patch("crewai_core.plus_api.PlusAPI._make_request")
|
|
@patch("crewai_core.plus_api.Settings")
|
|
def test_get_agent_with_org_uuid(mock_settings_class, mock_make_request):
|
|
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_make_request.return_value = mock_response
|
|
|
|
response = api.get_agent("test_agent_handle")
|
|
|
|
mock_make_request.assert_called_once_with(
|
|
"GET", "/crewai_plus/api/v1/agents/test_agent_handle"
|
|
)
|
|
assert "X-Crewai-Organization-Id" in api.headers
|
|
assert api.headers["X-Crewai-Organization-Id"] == org_uuid
|
|
assert response == mock_response
|
|
|
|
|
|
def _requests_sent_by(call: Callable[[PlusAPI], object]) -> list[httpx.Request]:
|
|
"""Run ``call`` against a PlusAPI whose HTTP client answers locally."""
|
|
sent: list[httpx.Request] = []
|
|
|
|
def answer(request: httpx.Request) -> httpx.Response:
|
|
sent.append(request)
|
|
return httpx.Response(200, json={})
|
|
|
|
transport = httpx.MockTransport(answer)
|
|
# A machine or OS proxy would mount a real transport ahead of the mock one.
|
|
with (
|
|
patch.dict("crewai_core.plus_api._MACHINE_TRANSPORT_ENV", clear=True),
|
|
patch("crewai_core.plus_api._os_proxies", dict),
|
|
patch(
|
|
"crewai_core.plus_api.httpx.Client",
|
|
side_effect=lambda **kwargs: _RealHttpxClient(transport=transport, **kwargs),
|
|
),
|
|
):
|
|
call(PlusAPI("test_api_key"))
|
|
return sent
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("call", "path"),
|
|
[
|
|
(
|
|
lambda api: api.get_triggers(),
|
|
"/crewai_plus/api/v1/integrations/apps",
|
|
),
|
|
(
|
|
lambda api: api.get_trigger_payload("hubspot", "contact_created"),
|
|
"/crewai_plus/api/v1/integrations/hubspot/contact_created/payload",
|
|
),
|
|
],
|
|
ids=["get_triggers", "get_trigger_payload"],
|
|
)
|
|
def test_trigger_requests_wait_60_seconds_only_for_the_response(call, path):
|
|
(request,) = _requests_sent_by(call)
|
|
|
|
assert request.url.path == path
|
|
assert request.extensions["timeout"] == {
|
|
"connect": 5.0,
|
|
"read": 60.0,
|
|
"write": 5.0,
|
|
"pool": 5.0,
|
|
}
|
|
|
|
|
|
def test_requests_without_their_own_timeout_keep_the_httpx_default():
|
|
(request,) = _requests_sent_by(lambda api: api.get_agent("an_agent"))
|
|
|
|
assert request.url.path == "/crewai_plus/api/v1/agents/an_agent"
|
|
assert request.extensions["timeout"]["read"] == 5.0
|
|
|
|
|
|
def test_an_export_waits_two_minutes_for_its_large_answer():
|
|
"""The export carries every setup's outputs and traces, tens of MB: the
|
|
httpx default of 5 s would cut it off."""
|
|
(request,) = _requests_sent_by(lambda api: api.export_evaluation("ev-1"))
|
|
|
|
assert request.method == "GET"
|
|
assert request.url.path == "/crewai_plus/api/v1/tracing/evaluations/ev-1/export"
|
|
assert request.extensions["timeout"] == {
|
|
"connect": 30.0,
|
|
"read": 120.0,
|
|
"write": 30.0,
|
|
"pool": 30.0,
|
|
}
|
|
|
|
|
|
def test_reading_an_evaluation_says_this_cli_can_export_it():
|
|
"""`can_export=1` lets the brief name `crewai eval export`: an AMP that does
|
|
not know the parameter drops it, and the brief stays silent about it."""
|
|
(request,) = _requests_sent_by(lambda api: api.get_evaluation("ev-1"))
|
|
|
|
assert request.method == "GET"
|
|
assert request.url.path == "/crewai_plus/api/v1/tracing/evaluations/ev-1"
|
|
assert dict(request.url.params) == {"can_export": "1"}
|
|
|
|
|
|
@pytest.mark.block_network(allowed_hosts=["127.0.0.1"])
|
|
def test_zip_deploy_uploads_go_through_the_proxy_the_machine_names(
|
|
tmp_path, monkeypatch
|
|
):
|
|
# Corporate networks and coding-agent sandboxes only allow traffic through
|
|
# the proxy in HTTP(S)_PROXY; the CLI's own ZIP upload must use it too.
|
|
# The machine's variables are the ones crewai-core pinned at startup.
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import threading
|
|
import zipfile
|
|
|
|
seen = []
|
|
|
|
class Proxy(BaseHTTPRequestHandler):
|
|
def do_POST(self):
|
|
body = self.rfile.read(int(self.headers["Content-Length"]))
|
|
seen.append((self.path, self.headers.get("Authorization"), body))
|
|
self.send_response(201)
|
|
self.send_header("Content-Length", "2")
|
|
self.end_headers()
|
|
self.wfile.write(b"{}")
|
|
|
|
def log_message(self, format, *args):
|
|
pass
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Proxy)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
monkeypatch.setattr(
|
|
"crewai_core.plus_api._MACHINE_TRANSPORT_ENV",
|
|
{"HTTP_PROXY": f"http://127.0.0.1:{server.server_address[1]}"},
|
|
)
|
|
project_zip = tmp_path / "project.zip"
|
|
with zipfile.ZipFile(project_zip, "w") as archive:
|
|
archive.writestr("pyproject.toml", "[project]\nname = 'demo'\n")
|
|
|
|
try:
|
|
response = PlusAPI("amp-token", base_url="http://amp.test").create_crew_from_zip(
|
|
project_zip, name="demo"
|
|
)
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
|
|
assert response.status_code == 201
|
|
((target, authorization, body),) = seen
|
|
assert target == "http://amp.test/crewai_plus/api/v1/crews/zip"
|
|
assert authorization == "Bearer amp-token"
|
|
assert b'filename="project.zip"' in body
|