diff --git a/lib/cli/tests/test_entrypoint_consistency.py b/lib/cli/tests/test_entrypoint_consistency.py new file mode 100644 index 000000000..081fcca34 --- /dev/null +++ b/lib/cli/tests/test_entrypoint_consistency.py @@ -0,0 +1,55 @@ +"""Tests ensuring the crewai and crewai-cli packages expose consistent entry points. + +Regression test for https://github.com/crewAIInc/crewAI/issues/6010: +`uv tool install crewai` failed because only crewai-cli declared [project.scripts]. +Both packages must declare the same entry point so that installing either one +via `uv tool install` exposes the `crewai` executable. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import tomllib + + +LIB_DIR = Path(__file__).resolve().parents[2] +CREWAI_PYPROJECT = LIB_DIR / "crewai" / "pyproject.toml" +CLI_PYPROJECT = LIB_DIR / "cli" / "pyproject.toml" + + +@pytest.fixture +def crewai_scripts() -> dict[str, str]: + data = tomllib.loads(CREWAI_PYPROJECT.read_text()) + return data.get("project", {}).get("scripts", {}) + + +@pytest.fixture +def cli_scripts() -> dict[str, str]: + data = tomllib.loads(CLI_PYPROJECT.read_text()) + return data.get("project", {}).get("scripts", {}) + + +def test_crewai_package_has_crewai_script(crewai_scripts: dict[str, str]) -> None: + """The crewai package must declare a 'crewai' script entry point.""" + assert "crewai" in crewai_scripts, ( + "lib/crewai/pyproject.toml must have [project.scripts] crewai = ... " + "so that `uv tool install crewai` exposes the crewai executable." + ) + + +def test_cli_package_has_crewai_script(cli_scripts: dict[str, str]) -> None: + """The crewai-cli package must declare a 'crewai' script entry point.""" + assert "crewai" in cli_scripts + + +def test_entrypoint_targets_same_function( + crewai_scripts: dict[str, str], + cli_scripts: dict[str, str], +) -> None: + """Both packages must point at the same CLI entry function.""" + assert crewai_scripts["crewai"] == cli_scripts["crewai"], ( + "The crewai and crewai-cli packages should declare the same " + "entry point target for the 'crewai' script." + ) diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index ff1d61b7f..ad51ca7ff 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -138,6 +138,9 @@ torchvision = [ crewai-files = { workspace = true } +[project.scripts] +crewai = "crewai_cli.cli:crewai" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/lib/crewai/tests/cli/test_entrypoint.py b/lib/crewai/tests/cli/test_entrypoint.py new file mode 100644 index 000000000..1575a68b5 --- /dev/null +++ b/lib/crewai/tests/cli/test_entrypoint.py @@ -0,0 +1,60 @@ +"""Tests ensuring the crewai package exposes the CLI entry point. + +Regression test for https://github.com/crewAIInc/crewAI/issues/6010: +`uv tool install crewai` failed because the crewai package did not declare +any [project.scripts], so uv could not find an executable to expose. +""" + +from __future__ import annotations + +import importlib +from pathlib import Path + +import click +import pytest +import tomllib + + +CREWAI_PYPROJECT = ( + Path(__file__).resolve().parents[2] / "pyproject.toml" +) + + +@pytest.fixture +def crewai_metadata() -> dict: + """Load the crewai package pyproject.toml as a dict.""" + return tomllib.loads(CREWAI_PYPROJECT.read_text()) + + +def test_crewai_package_declares_scripts_entrypoint(crewai_metadata: dict) -> None: + """The crewai package must declare a 'crewai' console script.""" + scripts = crewai_metadata.get("project", {}).get("scripts", {}) + assert "crewai" in scripts, ( + "The crewai package pyproject.toml must define [project.scripts] " + "with a 'crewai' entry so that `uv tool install crewai` works." + ) + + +def test_crewai_entrypoint_target_is_importable(crewai_metadata: dict) -> None: + """The target of the crewai script entry point must be importable.""" + scripts = crewai_metadata.get("project", {}).get("scripts", {}) + ref = scripts.get("crewai", "") + assert ":" in ref, f"Entry point reference should be 'module:attr', got: {ref!r}" + module_path, attr_name = ref.rsplit(":", 1) + mod = importlib.import_module(module_path) + entry = getattr(mod, attr_name, None) + assert entry is not None, ( + f"Could not find attribute {attr_name!r} in module {module_path!r}" + ) + + +def test_crewai_entrypoint_is_click_command(crewai_metadata: dict) -> None: + """The crewai CLI entry point must be a click command/group.""" + scripts = crewai_metadata.get("project", {}).get("scripts", {}) + ref = scripts["crewai"] + module_path, attr_name = ref.rsplit(":", 1) + mod = importlib.import_module(module_path) + entry = getattr(mod, attr_name) + assert isinstance(entry, click.BaseCommand), ( + f"Expected a click command/group, got {type(entry).__name__}" + )