mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 07:38:29 +00:00
This commit replaces .netrc with uv environment variables for installing tools from private repositories. To store credentials, I created a new and reusable settings file for the CLI in `$HOME/.config/crewai/settings.json`. The issue with .netrc files is that they are applied system-wide and are scoped by hostname, meaning we can't differentiate tool repositories requests from regular requests to CrewAI's API.
310 lines
10 KiB
Python
310 lines
10 KiB
Python
import os
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock
|
|
from contextlib import contextmanager
|
|
from io import StringIO
|
|
from unittest import mock
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from pytest import raises
|
|
|
|
from crewai.cli.tools.main import ToolCommand
|
|
|
|
|
|
@contextmanager
|
|
def in_temp_dir():
|
|
original_dir = os.getcwd()
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
os.chdir(temp_dir)
|
|
try:
|
|
yield temp_dir
|
|
finally:
|
|
os.chdir(original_dir)
|
|
|
|
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
def test_create_success(mock_subprocess):
|
|
with in_temp_dir():
|
|
tool_command = ToolCommand()
|
|
|
|
with patch.object(tool_command, "login") as mock_login, patch(
|
|
"sys.stdout", new=StringIO()
|
|
) as fake_out:
|
|
tool_command.create("test-tool")
|
|
output = fake_out.getvalue()
|
|
|
|
assert os.path.isdir("test_tool")
|
|
assert os.path.isfile(os.path.join("test_tool", "README.md"))
|
|
assert os.path.isfile(os.path.join("test_tool", "pyproject.toml"))
|
|
assert os.path.isfile(
|
|
os.path.join("test_tool", "src", "test_tool", "__init__.py")
|
|
)
|
|
assert os.path.isfile(os.path.join("test_tool", "src", "test_tool", "tool.py"))
|
|
|
|
with open(os.path.join("test_tool", "src", "test_tool", "tool.py"), "r") as f:
|
|
content = f.read()
|
|
assert "class TestTool" in content
|
|
|
|
mock_login.assert_called_once()
|
|
mock_subprocess.assert_called_once_with(["git", "init"], check=True)
|
|
|
|
assert "Creating custom tool test_tool..." in output
|
|
|
|
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
@patch("crewai.cli.plus_api.PlusAPI.get_tool")
|
|
def test_install_success(mock_get, mock_subprocess_run):
|
|
mock_get_response = MagicMock()
|
|
mock_get_response.status_code = 200
|
|
mock_get_response.json.return_value = {
|
|
"handle": "sample-tool",
|
|
"repository": {"handle": "sample-repo", "url": "https://example.com/repo"},
|
|
}
|
|
mock_get.return_value = mock_get_response
|
|
mock_subprocess_run.return_value = MagicMock(stderr=None)
|
|
|
|
tool_command = ToolCommand()
|
|
|
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
tool_command.install("sample-tool")
|
|
output = fake_out.getvalue()
|
|
|
|
mock_get.assert_has_calls([mock.call("sample-tool"), mock.call().json()])
|
|
mock_subprocess_run.assert_any_call(
|
|
[
|
|
"uv",
|
|
"add",
|
|
"--index",
|
|
"sample-repo=https://example.com/repo",
|
|
"sample-tool",
|
|
],
|
|
capture_output=False,
|
|
text=True,
|
|
check=True,
|
|
env=unittest.mock.ANY
|
|
)
|
|
|
|
assert "Succesfully installed sample-tool" in output
|
|
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI.get_tool")
|
|
def test_install_tool_not_found(mock_get):
|
|
mock_get_response = MagicMock()
|
|
mock_get_response.status_code = 404
|
|
mock_get.return_value = mock_get_response
|
|
|
|
tool_command = ToolCommand()
|
|
|
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
try:
|
|
tool_command.install("non-existent-tool")
|
|
except SystemExit:
|
|
pass
|
|
output = fake_out.getvalue()
|
|
|
|
mock_get.assert_called_once_with("non-existent-tool")
|
|
assert "No tool found with this name" in output
|
|
|
|
|
|
@patch("crewai.cli.plus_api.PlusAPI.get_tool")
|
|
def test_install_api_error(mock_get):
|
|
mock_get_response = MagicMock()
|
|
mock_get_response.status_code = 500
|
|
mock_get.return_value = mock_get_response
|
|
|
|
tool_command = ToolCommand()
|
|
|
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
try:
|
|
tool_command.install("error-tool")
|
|
except SystemExit:
|
|
pass
|
|
output = fake_out.getvalue()
|
|
|
|
mock_get.assert_called_once_with("error-tool")
|
|
assert "Failed to get tool details" in output
|
|
|
|
|
|
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False)
|
|
def test_publish_when_not_in_sync(mock_is_synced):
|
|
with patch("sys.stdout", new=StringIO()) as fake_out, raises(SystemExit):
|
|
tool_command = ToolCommand()
|
|
tool_command.publish(is_public=True)
|
|
|
|
assert "Local changes need to be resolved before publishing" in fake_out.getvalue()
|
|
|
|
|
|
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
|
@patch("crewai.cli.tools.main.get_project_version", return_value="1.0.0")
|
|
@patch("crewai.cli.tools.main.get_project_description", return_value="A sample tool")
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
@patch("crewai.cli.tools.main.os.listdir", return_value=["sample-tool-1.0.0.tar.gz"])
|
|
@patch(
|
|
"crewai.cli.tools.main.open",
|
|
new_callable=unittest.mock.mock_open,
|
|
read_data=b"sample tarball content",
|
|
)
|
|
@patch("crewai.cli.plus_api.PlusAPI.publish_tool")
|
|
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=False)
|
|
def test_publish_when_not_in_sync_and_force(
|
|
mock_is_synced,
|
|
mock_publish,
|
|
mock_open,
|
|
mock_listdir,
|
|
mock_subprocess_run,
|
|
mock_get_project_description,
|
|
mock_get_project_version,
|
|
mock_get_project_name,
|
|
):
|
|
mock_publish_response = MagicMock()
|
|
mock_publish_response.status_code = 200
|
|
mock_publish_response.json.return_value = {"handle": "sample-tool"}
|
|
mock_publish.return_value = mock_publish_response
|
|
|
|
tool_command = ToolCommand()
|
|
tool_command.publish(is_public=True, force=True)
|
|
|
|
mock_get_project_name.assert_called_with(require=True)
|
|
mock_get_project_version.assert_called_with(require=True)
|
|
mock_get_project_description.assert_called_with(require=False)
|
|
mock_subprocess_run.assert_called_with(
|
|
["uv", "build", "--sdist", "--out-dir", unittest.mock.ANY],
|
|
check=True,
|
|
capture_output=False,
|
|
)
|
|
mock_open.assert_called_with(unittest.mock.ANY, "rb")
|
|
mock_publish.assert_called_with(
|
|
handle="sample-tool",
|
|
is_public=True,
|
|
version="1.0.0",
|
|
description="A sample tool",
|
|
encoded_file=unittest.mock.ANY,
|
|
)
|
|
|
|
|
|
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
|
@patch("crewai.cli.tools.main.get_project_version", return_value="1.0.0")
|
|
@patch("crewai.cli.tools.main.get_project_description", return_value="A sample tool")
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
@patch("crewai.cli.tools.main.os.listdir", return_value=["sample-tool-1.0.0.tar.gz"])
|
|
@patch(
|
|
"crewai.cli.tools.main.open",
|
|
new_callable=unittest.mock.mock_open,
|
|
read_data=b"sample tarball content",
|
|
)
|
|
@patch("crewai.cli.plus_api.PlusAPI.publish_tool")
|
|
@patch("crewai.cli.tools.main.git.Repository.is_synced", return_value=True)
|
|
def test_publish_success(
|
|
mock_is_synced,
|
|
mock_publish,
|
|
mock_open,
|
|
mock_listdir,
|
|
mock_subprocess_run,
|
|
mock_get_project_description,
|
|
mock_get_project_version,
|
|
mock_get_project_name,
|
|
):
|
|
mock_publish_response = MagicMock()
|
|
mock_publish_response.status_code = 200
|
|
mock_publish_response.json.return_value = {"handle": "sample-tool"}
|
|
mock_publish.return_value = mock_publish_response
|
|
|
|
tool_command = ToolCommand()
|
|
tool_command.publish(is_public=True)
|
|
|
|
mock_get_project_name.assert_called_with(require=True)
|
|
mock_get_project_version.assert_called_with(require=True)
|
|
mock_get_project_description.assert_called_with(require=False)
|
|
mock_subprocess_run.assert_called_with(
|
|
["uv", "build", "--sdist", "--out-dir", unittest.mock.ANY],
|
|
check=True,
|
|
capture_output=False,
|
|
)
|
|
mock_open.assert_called_with(unittest.mock.ANY, "rb")
|
|
mock_publish.assert_called_with(
|
|
handle="sample-tool",
|
|
is_public=True,
|
|
version="1.0.0",
|
|
description="A sample tool",
|
|
encoded_file=unittest.mock.ANY,
|
|
)
|
|
|
|
|
|
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
|
@patch("crewai.cli.tools.main.get_project_version", return_value="1.0.0")
|
|
@patch("crewai.cli.tools.main.get_project_description", return_value="A sample tool")
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
@patch("crewai.cli.tools.main.os.listdir", return_value=["sample-tool-1.0.0.tar.gz"])
|
|
@patch(
|
|
"crewai.cli.tools.main.open",
|
|
new_callable=unittest.mock.mock_open,
|
|
read_data=b"sample tarball content",
|
|
)
|
|
@patch("crewai.cli.plus_api.PlusAPI.publish_tool")
|
|
def test_publish_failure(
|
|
mock_publish,
|
|
mock_open,
|
|
mock_listdir,
|
|
mock_subprocess_run,
|
|
mock_get_project_description,
|
|
mock_get_project_version,
|
|
mock_get_project_name,
|
|
):
|
|
mock_publish_response = MagicMock()
|
|
mock_publish_response.status_code = 422
|
|
mock_publish_response.json.return_value = {"name": ["is already taken"]}
|
|
mock_publish.return_value = mock_publish_response
|
|
|
|
tool_command = ToolCommand()
|
|
|
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
try:
|
|
tool_command.publish(is_public=True)
|
|
except SystemExit:
|
|
pass
|
|
output = fake_out.getvalue()
|
|
|
|
mock_publish.assert_called_once()
|
|
assert "Failed to complete operation" in output
|
|
assert "Name is already taken" in output
|
|
|
|
|
|
@patch("crewai.cli.tools.main.get_project_name", return_value="sample-tool")
|
|
@patch("crewai.cli.tools.main.get_project_version", return_value="1.0.0")
|
|
@patch("crewai.cli.tools.main.get_project_description", return_value="A sample tool")
|
|
@patch("crewai.cli.tools.main.subprocess.run")
|
|
@patch("crewai.cli.tools.main.os.listdir", return_value=["sample-tool-1.0.0.tar.gz"])
|
|
@patch(
|
|
"crewai.cli.tools.main.open",
|
|
new_callable=unittest.mock.mock_open,
|
|
read_data=b"sample tarball content",
|
|
)
|
|
@patch("crewai.cli.plus_api.PlusAPI.publish_tool")
|
|
def test_publish_api_error(
|
|
mock_publish,
|
|
mock_open,
|
|
mock_listdir,
|
|
mock_subprocess_run,
|
|
mock_get_project_description,
|
|
mock_get_project_version,
|
|
mock_get_project_name,
|
|
):
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 500
|
|
mock_response.json.return_value = {"error": "Internal Server Error"}
|
|
mock_response.ok = False
|
|
mock_publish.return_value = mock_response
|
|
|
|
tool_command = ToolCommand()
|
|
|
|
with patch("sys.stdout", new=StringIO()) as fake_out:
|
|
try:
|
|
tool_command.publish(is_public=True)
|
|
except SystemExit:
|
|
pass
|
|
output = fake_out.getvalue()
|
|
|
|
mock_publish.assert_called_once()
|
|
assert "Request to Enterprise API failed" in output
|