import os import shutil import tempfile from pathlib import Path import pytest from crewai.cli import utils @pytest.fixture def temp_tree(): root_dir = tempfile.mkdtemp() create_file(os.path.join(root_dir, "file1.txt"), "Hello, world!") create_file(os.path.join(root_dir, "file2.txt"), "Another file") os.mkdir(os.path.join(root_dir, "empty_dir")) nested_dir = os.path.join(root_dir, "nested_dir") os.mkdir(nested_dir) create_file(os.path.join(nested_dir, "nested_file.txt"), "Nested content") yield root_dir shutil.rmtree(root_dir) def create_file(path, content): with open(path, "w") as f: f.write(content) def test_tree_find_and_replace_file_content(temp_tree): utils.tree_find_and_replace(temp_tree, "world", "universe") with open(os.path.join(temp_tree, "file1.txt"), "r") as f: assert f.read() == "Hello, universe!" def test_tree_find_and_replace_file_name(temp_tree): old_path = os.path.join(temp_tree, "file2.txt") new_path = os.path.join(temp_tree, "file2_renamed.txt") os.rename(old_path, new_path) utils.tree_find_and_replace(temp_tree, "renamed", "modified") assert os.path.exists(os.path.join(temp_tree, "file2_modified.txt")) assert not os.path.exists(new_path) def test_tree_find_and_replace_directory_name(temp_tree): utils.tree_find_and_replace(temp_tree, "empty", "renamed") assert os.path.exists(os.path.join(temp_tree, "renamed_dir")) assert not os.path.exists(os.path.join(temp_tree, "empty_dir")) def test_tree_find_and_replace_nested_content(temp_tree): utils.tree_find_and_replace(temp_tree, "Nested", "Updated") with open(os.path.join(temp_tree, "nested_dir", "nested_file.txt"), "r") as f: assert f.read() == "Updated content" def test_tree_find_and_replace_no_matches(temp_tree): utils.tree_find_and_replace(temp_tree, "nonexistent", "replacement") assert set(os.listdir(temp_tree)) == { "file1.txt", "file2.txt", "empty_dir", "nested_dir", } def test_tree_copy_full_structure(temp_tree): dest_dir = tempfile.mkdtemp() try: utils.tree_copy(temp_tree, dest_dir) assert set(os.listdir(dest_dir)) == set(os.listdir(temp_tree)) assert os.path.isfile(os.path.join(dest_dir, "file1.txt")) assert os.path.isfile(os.path.join(dest_dir, "file2.txt")) assert os.path.isdir(os.path.join(dest_dir, "empty_dir")) assert os.path.isdir(os.path.join(dest_dir, "nested_dir")) assert os.path.isfile(os.path.join(dest_dir, "nested_dir", "nested_file.txt")) finally: shutil.rmtree(dest_dir) def test_tree_copy_preserve_content(temp_tree): dest_dir = tempfile.mkdtemp() try: utils.tree_copy(temp_tree, dest_dir) with open(os.path.join(dest_dir, "file1.txt"), "r") as f: assert f.read() == "Hello, world!" with open(os.path.join(dest_dir, "nested_dir", "nested_file.txt"), "r") as f: assert f.read() == "Nested content" finally: shutil.rmtree(dest_dir) def test_tree_copy_to_existing_directory(temp_tree): dest_dir = tempfile.mkdtemp() try: create_file(os.path.join(dest_dir, "existing_file.txt"), "I was here first") utils.tree_copy(temp_tree, dest_dir) assert os.path.isfile(os.path.join(dest_dir, "existing_file.txt")) assert os.path.isfile(os.path.join(dest_dir, "file1.txt")) finally: shutil.rmtree(dest_dir) @pytest.fixture def temp_project_dir(): """Create a temporary directory for testing tool extraction.""" with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) def create_init_file(directory, content): return create_file(directory / "__init__.py", content) def test_extract_available_exports_empty_project(temp_project_dir, capsys): with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "No valid tools were exposed in your __init__.py file" in captured.out def test_extract_available_exports_no_init_file(temp_project_dir, capsys): (temp_project_dir / "some_file.py").write_text("print('hello')") with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "No valid tools were exposed in your __init__.py file" in captured.out def test_extract_available_exports_empty_init_file(temp_project_dir, capsys): create_init_file(temp_project_dir, "") with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "Warning: No __all__ defined in" in captured.out def test_extract_available_exports_no_all_variable(temp_project_dir, capsys): create_init_file( temp_project_dir, "from crewai.tools import BaseTool\n\nclass MyTool(BaseTool):\n pass", ) with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "Warning: No __all__ defined in" in captured.out def test_extract_available_exports_valid_base_tool_class(temp_project_dir): create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" __all__ = ['MyTool'] """, ) tools = utils.extract_available_exports(dir_path=temp_project_dir) assert [{"name": "MyTool"}] == tools def test_extract_available_exports_valid_tool_decorator(temp_project_dir): create_init_file( temp_project_dir, """from crewai.tools import tool @tool def my_tool_function(text: str) -> str: \"\"\"A test tool function\"\"\" return text __all__ = ['my_tool_function'] """, ) tools = utils.extract_available_exports(dir_path=temp_project_dir) assert [{"name": "my_tool_function"}] == tools def test_extract_available_exports_multiple_valid_tools(temp_project_dir): create_init_file( temp_project_dir, """from crewai.tools import BaseTool, tool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" @tool def my_tool_function(text: str) -> str: \"\"\"A test tool function\"\"\" return text __all__ = ['MyTool', 'my_tool_function'] """, ) tools = utils.extract_available_exports(dir_path=temp_project_dir) assert [{"name": "MyTool"}, {"name": "my_tool_function"}] == tools def test_extract_available_exports_with_invalid_tool_decorator(temp_project_dir): create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" def not_a_tool(): pass __all__ = ['MyTool', 'not_a_tool'] """, ) tools = utils.extract_available_exports(dir_path=temp_project_dir) assert [{"name": "MyTool"}] == tools def test_extract_available_exports_import_error(temp_project_dir, capsys): create_init_file( temp_project_dir, """from nonexistent_module import something class MyTool(BaseTool): pass __all__ = ['MyTool'] """, ) with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "nonexistent_module" in captured.out def test_extract_available_exports_syntax_error(temp_project_dir, capsys): create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): # Missing closing parenthesis def __init__(self, name: pass __all__ = ['MyTool'] """, ) with pytest.raises(SystemExit): utils.extract_available_exports(dir_path=temp_project_dir) captured = capsys.readouterr() assert "was never closed" in captured.out @pytest.fixture def mock_crew(): from crewai.crew import Crew class MockCrew(Crew): def __init__(self): pass return MockCrew() @pytest.fixture def temp_crew_project(): with tempfile.TemporaryDirectory() as temp_dir: old_cwd = os.getcwd() os.chdir(temp_dir) crew_content = """ from crewai.crew import Crew from crewai.agent import Agent def create_crew() -> Crew: agent = Agent(role="test", goal="test", backstory="test") return Crew(agents=[agent], tasks=[]) # Direct crew instance direct_crew = Crew(agents=[], tasks=[]) """ with open("crew.py", "w") as f: f.write(crew_content) os.makedirs("src", exist_ok=True) with open(os.path.join("src", "crew.py"), "w") as f: f.write(crew_content) # Create a src/templates directory that should be ignored os.makedirs(os.path.join("src", "templates"), exist_ok=True) with open(os.path.join("src", "templates", "crew.py"), "w") as f: f.write("# This should be ignored") yield temp_dir os.chdir(old_cwd) def test_get_crews_finds_valid_crews(temp_crew_project, monkeypatch, mock_crew): def mock_fetch_crews(module_attr): return [mock_crew] monkeypatch.setattr(utils, "fetch_crews", mock_fetch_crews) crews = utils.get_crews() assert len(crews) > 0 assert mock_crew in crews def test_get_crews_with_nonexistent_file(temp_crew_project): crews = utils.get_crews(crew_path="nonexistent.py", require=False) assert len(crews) == 0 def test_get_crews_with_required_nonexistent_file(temp_crew_project, capsys): with pytest.raises(SystemExit): utils.get_crews(crew_path="nonexistent.py", require=True) captured = capsys.readouterr() assert "No valid Crew instance found" in captured.out def test_get_crews_with_invalid_module(temp_crew_project, capsys): with open("crew.py", "w") as f: f.write("import nonexistent_module\n") crews = utils.get_crews(crew_path="crew.py", require=False) assert len(crews) == 0 with pytest.raises(SystemExit): utils.get_crews(crew_path="crew.py", require=True) captured = capsys.readouterr() assert "Error" in captured.out def test_get_crews_ignores_template_directories( temp_crew_project, monkeypatch, mock_crew ): template_crew_detected = False def mock_fetch_crews(module_attr): nonlocal template_crew_detected if hasattr(module_attr, "__file__") and "templates" in module_attr.__file__: template_crew_detected = True return [mock_crew] monkeypatch.setattr(utils, "fetch_crews", mock_fetch_crews) utils.get_crews() assert not template_crew_detected # Tests for extract_tools_metadata def test_extract_tools_metadata_empty_project(temp_project_dir): """Test that extract_tools_metadata returns empty list for empty project.""" metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == [] def test_extract_tools_metadata_no_init_file(temp_project_dir): """Test that extract_tools_metadata returns empty list when no __init__.py exists.""" (temp_project_dir / "some_file.py").write_text("print('hello')") metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == [] def test_extract_tools_metadata_empty_init_file(temp_project_dir): """Test that extract_tools_metadata returns empty list for empty __init__.py.""" create_init_file(temp_project_dir, "") metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == [] def test_extract_tools_metadata_no_all_variable(temp_project_dir): """Test that extract_tools_metadata returns empty list when __all__ is not defined.""" create_init_file( temp_project_dir, "from crewai.tools import BaseTool\n\nclass MyTool(BaseTool):\n pass", ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == [] def test_extract_tools_metadata_valid_base_tool_class(temp_project_dir): """Test that extract_tools_metadata extracts metadata from a valid BaseTool class.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" __all__ = ['MyTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 assert metadata[0]["name"] == "MyTool" assert metadata[0]["humanized_name"] == "my_tool" assert metadata[0]["description"] == "A test tool" def test_extract_tools_metadata_with_args_schema(temp_project_dir): """Test that extract_tools_metadata extracts run_params_schema from args_schema.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool from pydantic import BaseModel class MyToolInput(BaseModel): query: str limit: int = 10 class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" args_schema: type[BaseModel] = MyToolInput __all__ = ['MyTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 assert metadata[0]["name"] == "MyTool" run_params = metadata[0]["run_params_schema"] assert "properties" in run_params assert "query" in run_params["properties"] assert "limit" in run_params["properties"] def test_extract_tools_metadata_with_env_vars(temp_project_dir): """Test that extract_tools_metadata extracts env_vars.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool from crewai.tools.base_tool import EnvVar class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" env_vars: list[EnvVar] = [ EnvVar(name="MY_API_KEY", description="API key for service", required=True), EnvVar(name="MY_OPTIONAL_VAR", description="Optional var", required=False, default="default_value"), ] __all__ = ['MyTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 env_vars = metadata[0]["env_vars"] assert len(env_vars) == 2 assert env_vars[0]["name"] == "MY_API_KEY" assert env_vars[0]["description"] == "API key for service" assert env_vars[0]["required"] is True assert env_vars[1]["name"] == "MY_OPTIONAL_VAR" assert env_vars[1]["required"] is False assert env_vars[1]["default"] == "default_value" def test_extract_tools_metadata_with_env_vars_field_default_factory(temp_project_dir): """Test that extract_tools_metadata extracts env_vars declared with Field(default_factory=...).""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool from crewai.tools.base_tool import EnvVar from pydantic import Field class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" env_vars: list[EnvVar] = Field( default_factory=lambda: [ EnvVar(name="MY_TOOL_API", description="API token for my tool", required=True), ] ) __all__ = ['MyTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 env_vars = metadata[0]["env_vars"] assert len(env_vars) == 1 assert env_vars[0]["name"] == "MY_TOOL_API" assert env_vars[0]["description"] == "API token for my tool" assert env_vars[0]["required"] is True def test_extract_tools_metadata_with_custom_init_params(temp_project_dir): """Test that extract_tools_metadata extracts init_params_schema with custom params.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" api_endpoint: str = "https://api.example.com" timeout: int = 30 __all__ = ['MyTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 init_params = metadata[0]["init_params_schema"] assert "properties" in init_params # Custom params should be included assert "api_endpoint" in init_params["properties"] assert "timeout" in init_params["properties"] # Base params should be filtered out assert "name" not in init_params["properties"] assert "description" not in init_params["properties"] def test_extract_tools_metadata_multiple_tools(temp_project_dir): """Test that extract_tools_metadata extracts metadata from multiple tools.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool class FirstTool(BaseTool): name: str = "first_tool" description: str = "First test tool" class SecondTool(BaseTool): name: str = "second_tool" description: str = "Second test tool" __all__ = ['FirstTool', 'SecondTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 2 names = [m["name"] for m in metadata] assert "FirstTool" in names assert "SecondTool" in names def test_extract_tools_metadata_multiple_init_files(temp_project_dir): """Test that extract_tools_metadata extracts metadata from multiple __init__.py files.""" # Create tool in root __init__.py create_init_file( temp_project_dir, """from crewai.tools import BaseTool class RootTool(BaseTool): name: str = "root_tool" description: str = "Root tool" __all__ = ['RootTool'] """, ) # Create nested package with another tool nested_dir = temp_project_dir / "nested" nested_dir.mkdir() create_init_file( nested_dir, """from crewai.tools import BaseTool class NestedTool(BaseTool): name: str = "nested_tool" description: str = "Nested tool" __all__ = ['NestedTool'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 2 names = [m["name"] for m in metadata] assert "RootTool" in names assert "NestedTool" in names def test_extract_tools_metadata_ignores_non_tool_exports(temp_project_dir): """Test that extract_tools_metadata ignores non-BaseTool exports.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): name: str = "my_tool" description: str = "A test tool" def not_a_tool(): pass SOME_CONSTANT = "value" __all__ = ['MyTool', 'not_a_tool', 'SOME_CONSTANT'] """, ) metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert len(metadata) == 1 assert metadata[0]["name"] == "MyTool" def test_extract_tools_metadata_import_error_returns_empty(temp_project_dir): """Test that extract_tools_metadata returns empty list on import error.""" create_init_file( temp_project_dir, """from nonexistent_module import something class MyTool(BaseTool): pass __all__ = ['MyTool'] """, ) # Should not raise, just return empty list metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == [] def test_extract_tools_metadata_syntax_error_returns_empty(temp_project_dir): """Test that extract_tools_metadata returns empty list on syntax error.""" create_init_file( temp_project_dir, """from crewai.tools import BaseTool class MyTool(BaseTool): # Missing closing parenthesis def __init__(self, name: pass __all__ = ['MyTool'] """, ) # Should not raise, just return empty list metadata = utils.extract_tools_metadata(dir_path=str(temp_project_dir)) assert metadata == []