"""Tests for crewai_cli.run_crew JSON crew handling.""" import os from pathlib import Path import subprocess import sys import pytest from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV import crewai_cli.run_crew as run_crew_module def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch): """crewai run -f must reach JSON crews, not only classic subprocess crews.""" monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: True) called: dict = {} def fake_run_json_crew_in_project_env(trained_agents_file=None): called["trained_agents_file"] = trained_agents_file monkeypatch.setattr( run_crew_module, "_run_json_crew_in_project_env", fake_run_json_crew_in_project_env, ) run_crew_module.run_crew(trained_agents_file="some.pkl") assert called == {"trained_agents_file": "some.pkl"} def test_json_run_uses_project_env_when_pyproject_exists(monkeypatch, tmp_path: Path): """JSON crew runs should execute inside the project uv environment.""" monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") install_calls = [] subprocess_calls = [] monkeypatch.setattr( run_crew_module, "_install_json_crew_dependencies_if_needed", lambda: install_calls.append(True), ) monkeypatch.setattr( run_crew_module, "build_env_with_all_tool_credentials", lambda: {"EXISTING": "value"}, ) def fake_subprocess_run(command, **kwargs): subprocess_calls.append((command, kwargs)) monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run) run_crew_module._run_json_crew_in_project_env( trained_agents_file="trained.pkl" ) expected_env = { "EXISTING": "value", run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str( Path(run_crew_module.__file__).resolve().parent ), CREWAI_TRAINED_AGENTS_FILE_ENV: "trained.pkl", } if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir(): expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str( local_crewai_source_dir ) assert install_calls == [True] assert subprocess_calls == [ ( [ "uv", "run", "--no-sync", "python", "-c", run_crew_module._JSON_CREW_RUNNER_CODE, ], { "capture_output": False, "text": True, "check": True, "env": expected_env, }, ) ] def test_json_run_uses_poetry_run_for_poetry_lock_without_uv_lock( monkeypatch, tmp_path: Path ): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") (tmp_path / "poetry.lock").write_text("# lock\n") monkeypatch.setattr( run_crew_module, "_install_json_crew_dependencies_if_needed", lambda: None, ) monkeypatch.setattr( run_crew_module, "build_env_with_all_tool_credentials", lambda: {}, ) subprocess_calls = [] def fake_subprocess_run(command, **kwargs): subprocess_calls.append((command, kwargs)) monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run) run_crew_module._run_json_crew_in_project_env() expected_env = { run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV: str( Path(run_crew_module.__file__).resolve().parent ), } if local_crewai_source_dir := run_crew_module._find_local_crewai_source_dir(): expected_env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str( local_crewai_source_dir ) assert subprocess_calls == [ ( [ "poetry", "run", "python", "-c", run_crew_module._JSON_CREW_RUNNER_CODE, ], { "capture_output": False, "text": True, "check": True, "env": expected_env, }, ) ] def test_json_runner_code_loads_current_cli_package_over_project_env(tmp_path: Path): old_parent = tmp_path / "old" old_pkg = old_parent / "crewai_cli" old_pkg.mkdir(parents=True) (old_pkg / "__init__.py").write_text("") (old_pkg / "run_crew.py").write_text("raise ImportError('old package used')\n") old_crewai_project = old_parent / "crewai" / "project" old_crewai_project.mkdir(parents=True) (old_parent / "crewai" / "__init__.py").write_text("") (old_crewai_project / "__init__.py").write_text("") (old_crewai_project / "json_loader.py").write_text( "raise ImportError('old crewai used')\n" ) current_pkg = tmp_path / "current" / "crewai_cli" current_pkg.mkdir(parents=True) marker = tmp_path / "marker.txt" (current_pkg / "__init__.py").write_text("") (current_pkg / "run_crew.py").write_text( "from pathlib import Path\n" "from crewai.project.json_loader import SOURCE\n" "def _run_json_crew(trained_agents_file=None):\n" f" Path({str(marker)!r}).write_text(SOURCE + ':' + (trained_agents_file or ''))\n" ) current_crewai_project = tmp_path / "current_crewai_src" / "crewai" / "project" current_crewai_project.mkdir(parents=True) (tmp_path / "current_crewai_src" / "crewai" / "__init__.py").write_text("") (current_crewai_project / "__init__.py").write_text("") (current_crewai_project / "json_loader.py").write_text("SOURCE = 'current'\n") env = os.environ.copy() env["PYTHONPATH"] = str(old_parent) env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(current_pkg) env[run_crew_module._CREWAI_RUNNER_SOURCE_DIR_ENV] = str( tmp_path / "current_crewai_src" ) env[CREWAI_TRAINED_AGENTS_FILE_ENV] = "trained.pkl" subprocess.run( [sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE], check=True, env=env, cwd=tmp_path, ) assert marker.read_text() == "current:trained.pkl" def test_json_run_without_pyproject_runs_in_process(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) called: dict = {} def fake_run_json_crew(trained_agents_file=None): called["trained_agents_file"] = trained_agents_file return "result" monkeypatch.setattr(run_crew_module, "_run_json_crew", fake_run_json_crew) assert ( run_crew_module._run_json_crew_in_project_env( trained_agents_file="trained.pkl" ) == "result" ) assert called == {"trained_agents_file": "trained.pkl"} def test_json_project_env_run_failure_exits_nonzero(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") monkeypatch.setattr( run_crew_module, "_install_json_crew_dependencies_if_needed", lambda: None ) monkeypatch.setattr( run_crew_module, "build_env_with_all_tool_credentials", lambda: {} ) def fake_subprocess_run(command, **kwargs): raise subprocess.CalledProcessError(7, command) monkeypatch.setattr(run_crew_module.subprocess, "run", fake_subprocess_run) with pytest.raises(SystemExit) as exc_info: run_crew_module._run_json_crew_in_project_env() assert exc_info.value.code == 7 def test_json_run_installs_dependencies_when_pyproject_has_no_lockfile( monkeypatch, tmp_path: Path ): """JSON crew runs should lock/sync project dependencies only when needed.""" monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") calls = [] def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): calls.append((proxy_options, raise_on_error, install_project)) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) run_crew_module._install_json_crew_dependencies_if_needed() assert calls == [([], True, None)] def test_json_run_syncs_frozen_when_uv_lock_exists_without_venv( monkeypatch, tmp_path: Path ): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") (tmp_path / "uv.lock").write_text("# lock\n") calls = [] def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): calls.append((proxy_options, raise_on_error, install_project)) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) run_crew_module._install_json_crew_dependencies_if_needed() assert calls == [(["--frozen"], True, None)] def test_json_run_skips_uv_sync_when_only_poetry_lock_exists_without_venv( monkeypatch, tmp_path: Path ): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") (tmp_path / "poetry.lock").write_text("# lock\n") calls = [] def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): calls.append((proxy_options, raise_on_error, install_project)) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) run_crew_module._install_json_crew_dependencies_if_needed() assert calls == [] @pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"]) def test_json_run_skips_dependency_install_when_lockfile_and_venv_exist( monkeypatch, tmp_path: Path, lockfile: str ): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") (tmp_path / lockfile).write_text("# lock\n") (tmp_path / ".venv").mkdir() calls = [] def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): calls.append((proxy_options, raise_on_error, install_project)) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) run_crew_module._install_json_crew_dependencies_if_needed() assert calls == [] def test_json_run_skips_dependency_install_without_pyproject( monkeypatch, tmp_path: Path ): monkeypatch.chdir(tmp_path) calls = [] def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): calls.append((proxy_options, raise_on_error)) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) run_crew_module._install_json_crew_dependencies_if_needed() assert calls == [] def test_json_run_install_failure_exits_nonzero(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") def fake_install_crew( proxy_options, *, raise_on_error=False, install_project=None ): raise subprocess.CalledProcessError(42, ["uv", "sync"]) monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) with pytest.raises(SystemExit) as exc_info: run_crew_module._install_json_crew_dependencies_if_needed() assert exc_info.value.code == 42 def test_run_json_crew_exports_trained_agents_env(monkeypatch, tmp_path: Path): """JSON crews run in-process, so the pickle path must land in the env var.""" monkeypatch.chdir(tmp_path) monkeypatch.delenv(CREWAI_TRAINED_AGENTS_FILE_ENV, raising=False) try: # No crew.json(c) in tmp_path: the loader fails *after* the env var # export, which is the part under test. with pytest.raises(FileNotFoundError): run_crew_module._run_json_crew(trained_agents_file="some.pkl") assert os.environ[CREWAI_TRAINED_AGENTS_FILE_ENV] == "some.pkl" finally: os.environ.pop(CREWAI_TRAINED_AGENTS_FILE_ENV, None) def test_run_json_crew_leaves_env_untouched_without_flag(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) monkeypatch.delenv(CREWAI_TRAINED_AGENTS_FILE_ENV, raising=False) with pytest.raises(FileNotFoundError): run_crew_module._run_json_crew() assert CREWAI_TRAINED_AGENTS_FILE_ENV not in os.environ def test_missing_input_names_accepts_hyphenated_placeholders(): """The prompt regex must accept the same names kickoff interpolation does.""" from types import SimpleNamespace crew = SimpleNamespace( agents=[ SimpleNamespace( role="Researcher", goal="Cover {my-topic}", backstory="" ) ], tasks=[ SimpleNamespace( description="Write about {my-topic} for {target-audience}", expected_output="Post", output_file=None, ) ], ) assert run_crew_module._missing_input_names(crew, {}) == [ "my-topic", "target-audience", ] def _patch_tui_run(monkeypatch, status: str): """Stub the TUI pieces of _run_json_crew so only exit handling runs.""" monkeypatch.delenv("CREWAI_DMN", raising=False) class FakeApp: def __init__(self, **kwargs): self._status = status self._crew_result = "result" if status == "completed" else None self._want_deploy = False def run(self): pass from types import SimpleNamespace crew = SimpleNamespace(name="Demo", tasks=[], agents=[]) monkeypatch.setattr( run_crew_module, "find_crew_json_file", lambda: Path("crew.jsonc") ) monkeypatch.setattr( run_crew_module, "_load_json_crew_for_tui", lambda _path: (FakeApp, crew, {}, [], []), ) monkeypatch.setattr( run_crew_module, "_prompt_for_missing_inputs", lambda _crew, inputs: inputs ) monkeypatch.setattr(run_crew_module, "_print_post_tui_summary", lambda _app: None) def test_run_json_crew_failed_status_exits_nonzero(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) _patch_tui_run(monkeypatch, status="failed") with pytest.raises(SystemExit) as exc_info: run_crew_module._run_json_crew() assert exc_info.value.code == 1 def test_run_json_crew_completed_status_returns_result(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) _patch_tui_run(monkeypatch, status="completed") assert run_crew_module._run_json_crew() == "result" def test_run_json_crew_dmn_mode_bypasses_tui(monkeypatch, tmp_path: Path, capsys): from types import SimpleNamespace monkeypatch.chdir(tmp_path) monkeypatch.setenv("CREWAI_DMN", "True") crew_path = tmp_path / "crew.jsonc" crew_path.write_text("{}") kickoff_calls = [] class FakeCrew: name = "Demo" agents = [SimpleNamespace(role="Researcher", goal="Research", backstory="")] tasks = [ SimpleNamespace( description="Research", expected_output="Findings", output_file=None, ) ] def kickoff(self, inputs): kickoff_calls.append(inputs) return "plain result" monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path) monkeypatch.setattr( run_crew_module, "_load_json_crew", lambda _path: (FakeCrew(), {"topic": "AI"}), ) monkeypatch.setattr( run_crew_module, "_load_json_crew_for_tui", lambda _path: pytest.fail("DMN mode must not start the TUI loader"), ) assert run_crew_module._run_json_crew() == "plain result" captured = capsys.readouterr() assert kickoff_calls == [{"topic": "AI"}] assert "plain result" in captured.out def test_run_json_crew_dmn_mode_exits_on_missing_inputs( monkeypatch, tmp_path: Path, capsys ): from types import SimpleNamespace monkeypatch.chdir(tmp_path) monkeypatch.setenv("CREWAI_DMN", "True") crew_path = tmp_path / "crew.jsonc" crew_path.write_text("{}") crew = SimpleNamespace( agents=[ SimpleNamespace( role="Researcher", goal="Research {topic}", backstory="", ) ], tasks=[], ) monkeypatch.setattr(run_crew_module, "find_crew_json_file", lambda: crew_path) monkeypatch.setattr( run_crew_module, "_load_json_crew", lambda _path: (crew, {}), ) with pytest.raises(SystemExit) as exc_info: run_crew_module._run_json_crew() captured = capsys.readouterr() assert exc_info.value.code == 1 assert "Missing runtime inputs for CREWAI_DMN mode: topic" in captured.err def test_has_json_crew_defers_to_declared_flow_type(monkeypatch, tmp_path: Path): """A flow project containing a stray crew.jsonc must still run as a flow.""" monkeypatch.chdir(tmp_path) (tmp_path / "crew.jsonc").write_text("{}") (tmp_path / "pyproject.toml").write_text('[tool.crewai]\ntype = "flow"\n') assert run_crew_module._has_json_crew() is False def test_has_json_crew_true_for_declared_crew_type(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) (tmp_path / "crew.jsonc").write_text("{}") (tmp_path / "pyproject.toml").write_text('[tool.crewai]\ntype = "crew"\n') assert run_crew_module._has_json_crew() is True def test_has_json_crew_true_without_pyproject(monkeypatch, tmp_path: Path): monkeypatch.chdir(tmp_path) (tmp_path / "crew.jsonc").write_text("{}") assert run_crew_module._has_json_crew() is True