fix: normalize project names by stripping trailing slashes in crew creation

- 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 <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-24 18:37:40 +00:00
parent 060c486948
commit e7872f02c4
2 changed files with 167 additions and 0 deletions

View File

@@ -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(" ", "")

View File

@@ -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()