From e7872f02c4024beb17e0cfe0a29efb3f181e3027 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:37:40 +0000 Subject: [PATCH] fix: normalize project names by stripping trailing slashes in crew creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Strip trailing slashes from project names in create_folder_structure - Add comprehensive tests for trailing slash scenarios - Fixes #3059 The issue occurred because trailing slashes in project names like 'hello/' were directly incorporated into pyproject.toml, creating invalid package names and script entries. This fix silently normalizes project names by stripping trailing slashes before processing, maintaining backward compatibility while fixing the invalid template generation. Co-Authored-By: João --- src/crewai/cli/create_crew.py | 1 + tests/cli/test_create_crew.py | 166 ++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 tests/cli/test_create_crew.py diff --git a/src/crewai/cli/create_crew.py b/src/crewai/cli/create_crew.py index c658b0de1..362c41517 100644 --- a/src/crewai/cli/create_crew.py +++ b/src/crewai/cli/create_crew.py @@ -14,6 +14,7 @@ from crewai.cli.utils import copy_template, load_env_vars, write_env_file def create_folder_structure(name, parent_folder=None): + name = name.rstrip('/') folder_name = name.replace(" ", "_").replace("-", "_").lower() class_name = name.replace("_", " ").replace("-", " ").title().replace(" ", "") diff --git a/tests/cli/test_create_crew.py b/tests/cli/test_create_crew.py new file mode 100644 index 000000000..027053801 --- /dev/null +++ b/tests/cli/test_create_crew.py @@ -0,0 +1,166 @@ +import os +import shutil +import tempfile +from pathlib import Path +from unittest import mock + +import pytest +from click.testing import CliRunner + +from crewai.cli.create_crew import create_crew, create_folder_structure + + +@pytest.fixture +def runner(): + return CliRunner() + + +@pytest.fixture +def temp_dir(): + temp_path = tempfile.mkdtemp() + yield temp_path + shutil.rmtree(temp_path) + + +def test_create_folder_structure_strips_single_trailing_slash(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + folder_path, folder_name, class_name = create_folder_structure("hello/") + + assert folder_name == "hello" + assert class_name == "Hello" + assert folder_path.name == "hello" + assert folder_path.exists() + + +def test_create_folder_structure_strips_multiple_trailing_slashes(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + folder_path, folder_name, class_name = create_folder_structure("hello///") + + assert folder_name == "hello" + assert class_name == "Hello" + assert folder_path.name == "hello" + assert folder_path.exists() + + +def test_create_folder_structure_handles_complex_name_with_trailing_slash(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + folder_path, folder_name, class_name = create_folder_structure("my-awesome_project/") + + assert folder_name == "my_awesome_project" + assert class_name == "MyAwesomeProject" + assert folder_path.name == "my_awesome_project" + assert folder_path.exists() + + +def test_create_folder_structure_normal_name_unchanged(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + folder_path, folder_name, class_name = create_folder_structure("hello") + + assert folder_name == "hello" + assert class_name == "Hello" + assert folder_path.name == "hello" + assert folder_path.exists() + + + + + +def test_create_folder_structure_with_parent_folder(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + parent_path = Path(temp_dir) / "parent" + parent_path.mkdir() + + folder_path, folder_name, class_name = create_folder_structure("child/", parent_folder=parent_path) + + assert folder_name == "child" + assert class_name == "Child" + assert folder_path.name == "child" + assert folder_path.parent == parent_path + assert folder_path.exists() + + +@mock.patch("crewai.cli.create_crew.copy_template") +@mock.patch("crewai.cli.create_crew.write_env_file") +@mock.patch("crewai.cli.create_crew.load_env_vars") +def test_create_crew_with_trailing_slash_creates_valid_project(mock_load_env, mock_write_env, mock_copy_template, temp_dir): + mock_load_env.return_value = {} + + with tempfile.TemporaryDirectory() as work_dir: + os.chdir(work_dir) + create_crew("test-project/", skip_provider=True) + + project_path = Path(work_dir) / "test_project" + assert project_path.exists() + assert (project_path / "src" / "test_project").exists() + + mock_copy_template.assert_called() + copy_calls = mock_copy_template.call_args_list + + for call in copy_calls: + args = call[0] + if len(args) >= 5: + folder_name_arg = args[4] + assert not folder_name_arg.endswith("/"), f"folder_name should not end with slash: {folder_name_arg}" + + +@mock.patch("crewai.cli.create_crew.copy_template") +@mock.patch("crewai.cli.create_crew.write_env_file") +@mock.patch("crewai.cli.create_crew.load_env_vars") +def test_create_crew_with_multiple_trailing_slashes(mock_load_env, mock_write_env, mock_copy_template, temp_dir): + mock_load_env.return_value = {} + + with tempfile.TemporaryDirectory() as work_dir: + os.chdir(work_dir) + create_crew("test-project///", skip_provider=True) + + project_path = Path(work_dir) / "test_project" + assert project_path.exists() + assert (project_path / "src" / "test_project").exists() + + +@mock.patch("crewai.cli.create_crew.copy_template") +@mock.patch("crewai.cli.create_crew.write_env_file") +@mock.patch("crewai.cli.create_crew.load_env_vars") +def test_create_crew_normal_name_still_works(mock_load_env, mock_write_env, mock_copy_template, temp_dir): + mock_load_env.return_value = {} + + with tempfile.TemporaryDirectory() as work_dir: + os.chdir(work_dir) + create_crew("normal-project", skip_provider=True) + + project_path = Path(work_dir) / "normal_project" + assert project_path.exists() + assert (project_path / "src" / "normal_project").exists() + + +def test_create_folder_structure_handles_spaces_and_dashes_with_slash(): + with tempfile.TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + folder_path, folder_name, class_name = create_folder_structure("My Cool-Project/") + + assert folder_name == "my_cool_project" + assert class_name == "MyCoolProject" + assert folder_path.name == "my_cool_project" + assert folder_path.exists() + + +@mock.patch("crewai.cli.create_crew.copy_template") +@mock.patch("crewai.cli.create_crew.write_env_file") +@mock.patch("crewai.cli.create_crew.load_env_vars") +def test_create_crew_with_parent_folder_and_trailing_slash(mock_load_env, mock_write_env, mock_copy_template, temp_dir): + mock_load_env.return_value = {} + + with tempfile.TemporaryDirectory() as work_dir: + parent_path = Path(work_dir) / "parent" + parent_path.mkdir() + + create_crew("child-crew/", skip_provider=True, parent_folder=parent_path) + + crew_path = parent_path / "child_crew" + assert crew_path.exists() + assert not (crew_path / "src").exists()