diff --git a/src/crewai/cli/run_crew.py b/src/crewai/cli/run_crew.py index 48216ab75..fee472ffc 100644 --- a/src/crewai/cli/run_crew.py +++ b/src/crewai/cli/run_crew.py @@ -1,3 +1,4 @@ +import logging import subprocess import sys from enum import Enum @@ -10,6 +11,8 @@ from packaging import version from crewai.cli.utils import read_toml from crewai.cli.version import get_crewai_version +logger = logging.getLogger(__name__) + class CrewType(Enum): STANDARD = "standard" @@ -50,17 +53,30 @@ def run_crew() -> None: def execute_command(crew_type: CrewType) -> None: - """ - Execute the appropriate command based on crew type. + """Execute the appropriate command based on crew type. Args: crew_type: The type of crew to run + + Note: + Automatically adds the 'src' directory to sys.path if it exists + to ensure proper module resolution. """ # Add the 'src' directory to sys.path to ensure module imports work correctly cwd = Path.cwd() src_path = cwd / "src" + + logger.debug(f"Current working directory: {cwd}") + if src_path.exists() and str(src_path) not in sys.path: + logger.info(f"Adding {src_path} to sys.path") sys.path.insert(0, str(src_path)) + elif not src_path.exists(): + logger.warning(f"src directory not found: {src_path}") + click.secho( + f"Warning: 'src' directory not found at {src_path}. Module imports may fail.", + fg="yellow", + ) command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"] @@ -71,6 +87,7 @@ def execute_command(crew_type: CrewType) -> None: handle_error(e, crew_type) except Exception as e: + logger.error(f"Unexpected error: {e}") click.echo(f"An unexpected error occurred: {e}", err=True) diff --git a/tests/cli/test_run_crew.py b/tests/cli/test_run_crew.py index bd6da9c9c..770c37bf9 100644 --- a/tests/cli/test_run_crew.py +++ b/tests/cli/test_run_crew.py @@ -10,7 +10,14 @@ from crewai.cli.run_crew import CrewType, execute_command def test_execute_command_adds_src_to_path(): - """Test that execute_command adds the src directory to sys.path.""" + """ + Test that execute_command correctly modifies sys.path. + + Ensures: + 1. src directory is added to sys.path when it exists. + 2. Original sys.path is preserved for other entries. + 3. Command execution proceeds correctly. + """ # Create a temporary directory with a src subdirectory with tempfile.TemporaryDirectory() as temp_dir: temp_path = Path(temp_dir) diff --git a/tests/cli/test_run_crew_edge_cases.py b/tests/cli/test_run_crew_edge_cases.py new file mode 100644 index 000000000..f3727c3af --- /dev/null +++ b/tests/cli/test_run_crew_edge_cases.py @@ -0,0 +1,115 @@ +import os +import subprocess +import sys +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from crewai.cli.run_crew import CrewType, execute_command + + +@pytest.mark.parametrize("crew_type", [CrewType.STANDARD, CrewType.FLOW]) +def test_execute_command_with_different_crew_types(crew_type): + """ + Test that execute_command works with different crew types. + + Verifies that the correct command is executed based on the crew type. + """ + # Create a temporary directory with a src subdirectory + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + src_path = temp_path / "src" + src_path.mkdir() + + # Change to the temporary directory + original_dir = os.getcwd() + os.chdir(temp_dir) + + try: + # Save the original sys.path + original_sys_path = sys.path.copy() + + # Mock subprocess.run to avoid actually running the command + with patch("subprocess.run") as mock_run: + # Call execute_command + execute_command(crew_type) + + # Check that the correct command was called based on crew_type + expected_command = ["uv", "run", "kickoff" if crew_type == CrewType.FLOW else "run_crew"] + mock_run.assert_called_once_with(expected_command, capture_output=False, text=True, check=True) + finally: + # Restore the original directory and sys.path + os.chdir(original_dir) + sys.path = original_sys_path + + +def test_execute_command_handles_missing_src_directory(): + """ + Test that execute_command handles missing src directory gracefully. + + Verifies that the command executes even when the src directory doesn't exist. + """ + # Create a temporary directory without a src subdirectory + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Change to the temporary directory + original_dir = os.getcwd() + os.chdir(temp_dir) + + try: + # Save the original sys.path + original_sys_path = sys.path.copy() + + # Mock subprocess.run to avoid actually running the command + with patch("subprocess.run") as mock_run: + # Call execute_command + execute_command(CrewType.STANDARD) + + # Check that sys.path wasn't modified (since src doesn't exist) + assert sys.path == original_sys_path + + # Check that the command was still called + mock_run.assert_called_once_with(["uv", "run", "run_crew"], capture_output=False, text=True, check=True) + finally: + # Restore the original directory and sys.path + os.chdir(original_dir) + sys.path = original_sys_path + + +def test_execute_command_handles_subprocess_error(): + """ + Test that execute_command properly handles subprocess errors. + + Verifies that exceptions from subprocess.run are propagated correctly. + """ + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + src_path = temp_path / "src" + src_path.mkdir() + + # Create a dummy pyproject.toml file + with open(temp_path / "pyproject.toml", "w") as f: + f.write("[project]\nname = \"test\"\n") + + # Change to the temporary directory + original_dir = os.getcwd() + os.chdir(temp_dir) + + try: + # Save the original sys.path + original_sys_path = sys.path.copy() + + # Mock subprocess.run to raise an exception + with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, [])): + # Call execute_command and verify it handles the error + execute_command(CrewType.STANDARD) + + # Verify that sys.path was modified correctly + assert str(src_path) in sys.path + finally: + # Restore the original directory and sys.path + os.chdir(original_dir) + sys.path = original_sys_path