Files
crewAI/lib/cli/tests/test_run_crew.py
João Moura 3246cb30f5 fix(cli): unify crewai run flow input resolution and prompt from the state schema (#6466)
* fix(cli): unify `crewai run` flow input resolution; prompt from state schema

`crewai run` resolved the configured [tool.crewai] flow, but `--inputs` was
hard-gated behind `--definition` and routed through a separate branch — the two
ways of pointing at the same flow didn't share resolution, and required inputs
were never detected, prompted, or validated (a missing field only blew up at
runtime).

Now inputs and definition come from one place:

- Remove the "--inputs requires --definition" gate (cli.py, run_crew.py,
  run_declarative_flow.py). `--inputs` alone resolves the configured flow,
  exactly like a bare `crewai run`; `--definition` is purely an override. The
  project-env re-exec forwards `--inputs` instead of rejecting it.
- Read the flow's state schema from the runtime Flow instance
  (`type(flow.state).model_json_schema()`), which is reliable for both inline
  `json_schema` and ref-imported `pydantic` state (the static definition's
  json_schema is None for the common ref case).
- Plain `crewai run` detects required state fields (minus those satisfied by
  state defaults) and prompts for them interactively, showing each field's
  description; skipped in non-interactive / CREWAI_DMN mode.
- Validate against the schema before kickoff: pointed
  "Missing required input 'x' — <description>" errors, and warn on unknown keys
  with a did-you-mean suggestion (catches typos like `prospect_emai`).

`--inputs` on a non-flow project now errors clearly ("only supported for
declarative flows") instead of the old confusing gate.

Tests: schema-driven prompt/validate/override paths, unknown-key warning,
defaults-satisfy-required, type validation, and re-exec input forwarding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* fix(cli): forward reserved `id` input to flow kickoff; ruff format

- Cursor: the schema filter treated an `id` key in --inputs as unknown and
  dropped it, regressing kickoff's persistence-restore support (inputs["id"]).
  Let `id` pass through untouched (test: reserved_id_input_is_forwarded).
- Apply ruff format to run_declarative_flow.py (fixes the lint-run
  `ruff format --check` step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* fix(cli): don't block persistence-restore resume on schema validation

Cursor (High): `crewai run --inputs '{"id":"…"}'` is a persistence resume —
kickoff hydrates full state from storage, so schema-required fields may come
from the restored state rather than --inputs. The new required-field
prompt/validation was erroring/prompting before kickoff, breaking resume. When
`id` is present in --inputs, forward the inputs unchanged and skip the
prompt/validation. Test: test_id_only_input_skips_required_validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* fix(cli): load project .env in the declarative-flow runner

The declarative-flow path never loaded .env — flow projects (type = "flow")
missed API keys/config that crew projects pick up. The JSON-crew path loads
Path.cwd()/.env with override=True (run_crew._run_json_crew); mirror that at
the top of run_declarative_flow() so flow projects behave the same regardless
of where crewai is installed. Test: run_declarative_flow_loads_project_env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* feat(cli): unify runtime-input prompting across declarative flows and crews

Declarative (JSON) crews now resolve inputs the same way declarative flows
do, via a shared crewai_cli.input_prompt module (prompt_for_inputs,
parse_inputs_json, closest_name, is_interactive):

- accept --inputs (previously rejected for crews), forwarded to the crew
  subprocess via CREWAI_JSON_CREW_INPUTS and validated before spinning up uv
- layer --inputs over the crew's declared `inputs` defaults
- prompt for missing {placeholder}s with the same UX as flows, and error
  cleanly with a pointed per-name message when non-interactive
- warn on unknown keys with a "did you mean" suggestion

Unlike flows — whose state schema is authoritative, so unknown keys are
dropped — the crew placeholder scan is heuristic (agent/task text fields
only), so unrecognized keys are warned about but kept, to avoid discarding a
value a field the scan doesn't cover may rely on.

--inputs remains rejected for classic (Python/YAML) crews, which take their
inputs from main.py. run_declarative_flow's private input helpers move to the
shared module with no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* test(crewai): update mirrored CLI test after run_crew input refactor

lib/crewai/tests/cli/test_run_crew.py imports crewai_cli internals and is
collected by the lib/crewai test job (Run Tests). It still imported
_prompt_for_missing_inputs, which was replaced by _resolve_crew_inputs, so
the module failed to import — erroring pytest at collection and cancelling
the rest of the matrix via fail-fast.

Point it at _resolve_crew_inputs and patch the prompt in the shared
crewai_cli.input_prompt module where prompting now lives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

* fix(cli): filter unknown --inputs keys even on flow persistence restore

Review follow-up: the `id` (persistence-restore) branch of
_resolve_flow_inputs returned the raw payload, so typo keys passed alongside
`id` skipped the unknown-key warning/drop and reached kickoff — which can
fail strict (extra="forbid") flow state models. The restore path now still
warns on and drops unknown keys (keeping `id` and known state fields); it
only skips the required-field prompt and pre-kickoff validation, which
persistence hydrates. Regression test: test_id_restore_still_drops_unknown_keys.

Also drop the duplicate module import in test_input_prompt.py (both `import`
and `from ... import` of crewai_cli.input_prompt) flagged by the code-quality
bot; monkeypatching now uses the string target form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:17:09 -03:00

1063 lines
33 KiB
Python

"""Tests for crewai_cli.run_crew JSON crew handling."""
import os
from pathlib import Path
import subprocess
import sys
import click
import pytest
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
import crewai_cli.input_prompt as input_prompt_module
import crewai_cli.run_crew as run_crew_module
def test_missing_crewai_package_shows_full_install_hint(monkeypatch):
def missing_crewai_package():
raise ModuleNotFoundError("No module named 'crewai'", name="crewai")
real_import = __import__
def fake_import(name, *args, **kwargs):
if name == "crewai.project.crew_loader":
missing_crewai_package()
return real_import(name, *args, **kwargs)
monkeypatch.setattr("builtins.__import__", fake_import)
with pytest.raises(click.ClickException) as exc_info:
run_crew_module._load_json_crew(Path("crew.jsonc"))
message = exc_info.value.message
assert "CrewAI CLI is installed without the `crewai` package" in message
assert "uv tool install --force 'crewai[tools]>=1.15.0,<2.0.0'" in message
assert "quotes are required in zsh" in message
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, "read_toml", lambda: {})
monkeypatch.setattr(
run_crew_module,
"configured_project_json_crew",
lambda pyproject_data=None, project_root=None: Path("crew.jsonc"),
)
called: dict = {}
def fake_run_json_crew_in_project_env(
trained_agents_file=None, crew_path=None, inputs=None
):
called["trained_agents_file"] = trained_agents_file
called["crew_path"] = crew_path
called["inputs"] = inputs
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",
"crew_path": Path("crew.jsonc"),
"inputs": None,
}
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)
crew_path = tmp_path / "crew.jsonc"
run_crew_module._run_json_crew_in_project_env(
trained_agents_file="trained.pkl",
crew_path=crew_path,
)
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",
run_crew_module._CREWAI_JSON_CREW_DEFINITION_ENV: str(crew_path),
}
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_runner_imports_with_older_project_env_crewai_core(tmp_path: Path):
old_parent = tmp_path / "old_env"
old_crewai_core = old_parent / "crewai_core"
old_crewai_core.mkdir(parents=True)
(old_crewai_core / "__init__.py").write_text("")
(old_crewai_core / "constants.py").write_text(
"CREWAI_TRAINED_AGENTS_FILE_ENV = 'CREWAI_TRAINED_AGENTS_FILE'\n"
)
(old_crewai_core / "project.py").write_text(
"def read_toml(*args, **kwargs):\n"
" return {}\n"
"def parse_toml(*args, **kwargs):\n"
" return {}\n"
"def get_project_description(*args, **kwargs):\n"
" return None\n"
"def get_project_name(*args, **kwargs):\n"
" return None\n"
"def get_project_version(*args, **kwargs):\n"
" return None\n"
)
(old_crewai_core / "tool_credentials.py").write_text(
"def build_env_with_all_tool_credentials(*args, **kwargs):\n"
" return {}\n"
"def build_env_with_tool_repository_credentials(*args, **kwargs):\n"
" return {}\n"
)
(old_crewai_core / "version.py").write_text(
"def check_version(*args, **kwargs):\n"
" return None\n"
"def get_crewai_version(*args, **kwargs):\n"
" return '1.0.0'\n"
"def get_latest_version_from_pypi(*args, **kwargs):\n"
" return None\n"
"def is_current_version_yanked(*args, **kwargs):\n"
" return False\n"
"def is_newer_version_available(*args, **kwargs):\n"
" return False\n"
)
marker = tmp_path / "marker.txt"
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 / "crew_loader.py").write_text(
"from pathlib import Path\n"
"class Crew:\n"
" agents = []\n"
" tasks = []\n"
" def kickoff(self, inputs):\n"
f" Path({str(marker)!r}).write_text('ran')\n"
" return 'done'\n"
"def load_crew(path):\n"
" return Crew(), {}\n"
)
env = os.environ.copy()
env["PYTHONPATH"] = str(old_parent)
env["CREWAI_DMN"] = "true"
env[run_crew_module._CREWAI_CLI_RUNNER_PACKAGE_DIR_ENV] = str(
Path(run_crew_module.__file__).resolve().parent
)
env[run_crew_module._CREWAI_JSON_CREW_DEFINITION_ENV] = "crew.jsonc"
subprocess.run(
[sys.executable, "-c", run_crew_module._JSON_CREW_RUNNER_CODE],
check=True,
env=env,
cwd=tmp_path,
)
assert marker.read_text() == "ran"
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, crew_path=None, inputs=None):
called["trained_agents_file"] = trained_agents_file
called["crew_path"] = crew_path
called["inputs"] = inputs
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",
"crew_path": None,
"inputs": None,
}
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, "configured_project_json_crew", lambda: Path("crew.jsonc")
)
monkeypatch.setattr(
run_crew_module,
"_load_json_crew_for_tui",
lambda _path: (FakeApp, crew, {}, [], []),
)
monkeypatch.setattr(
run_crew_module,
"_resolve_crew_inputs",
lambda _crew, default_inputs, _provided, *, interactive: default_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, "configured_project_json_crew", 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, "configured_project_json_crew", 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 required input 'topic'" in captured.err
# ── Declarative-crew inputs: merge --inputs, warn, prompt (flow parity) ──
def _crew_with_placeholders(*names: str) -> object:
"""A minimal crew whose agent goal references each ``{name}`` placeholder."""
from types import SimpleNamespace
goal = " ".join(f"{{{name}}}" for name in names)
return SimpleNamespace(
agents=[SimpleNamespace(role="Researcher", goal=goal, backstory="")],
tasks=[],
)
def test_resolve_crew_inputs_merges_inputs_over_defaults():
crew = _crew_with_placeholders("topic")
resolved = run_crew_module._resolve_crew_inputs(
crew, {"topic": "AI"}, {"topic": "ML"}, interactive=False
)
assert resolved == {"topic": "ML"}
def test_resolve_crew_inputs_warns_and_keeps_unknown_input(capsys):
# The placeholder scan is heuristic, so an unreferenced key is flagged but
# kept (never silently dropped like a flow's schema would).
crew = _crew_with_placeholders("topic")
resolved = run_crew_module._resolve_crew_inputs(
crew, {}, {"topic": "AI", "topi": "typo"}, interactive=False
)
captured = capsys.readouterr()
assert resolved == {"topic": "AI", "topi": "typo"}
assert "isn't referenced by any {placeholder}" in captured.err
assert "Did you mean 'topic'?" in captured.err
def test_resolve_crew_inputs_prompts_when_interactive(monkeypatch):
crew = _crew_with_placeholders("topic")
prompted: list[str] = []
def fake_prompt(text: str, **kwargs: object) -> str:
prompted.append(text)
return "AI"
monkeypatch.setattr(input_prompt_module.click, "prompt", fake_prompt)
resolved = run_crew_module._resolve_crew_inputs(
crew, {}, None, interactive=True
)
assert resolved == {"topic": "AI"}
assert any("topic" in text for text in prompted)
def test_resolve_crew_inputs_errors_when_missing_non_interactive(capsys):
crew = _crew_with_placeholders("topic")
with pytest.raises(SystemExit) as exc_info:
run_crew_module._resolve_crew_inputs(crew, {}, None, interactive=False)
assert exc_info.value.code == 1
assert "Missing required input 'topic'" in capsys.readouterr().err
def test_run_json_crew_accepts_inputs_argument(monkeypatch, tmp_path: Path, capsys):
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CREWAI_DMN", "True")
crew_path = tmp_path / "crew.jsonc"
crew_path.write_text("{}")
kickoff_calls: list[dict] = []
class FakeCrew:
name = "Demo"
agents = [_crew_with_placeholders("topic").agents[0]]
tasks: list = []
def kickoff(self, inputs):
kickoff_calls.append(inputs)
return "ok"
monkeypatch.setattr(
run_crew_module, "configured_project_json_crew", lambda: crew_path
)
monkeypatch.setattr(
run_crew_module, "_load_json_crew", lambda _path: (FakeCrew(), {})
)
assert run_crew_module._run_json_crew(inputs='{"topic":"AI"}') == "ok"
assert kickoff_calls == [{"topic": "AI"}]
def test_run_json_crew_in_project_env_forwards_inputs(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: {}
)
captured_kwargs: list[dict] = []
monkeypatch.setattr(
run_crew_module.subprocess,
"run",
lambda command, **kwargs: captured_kwargs.append(kwargs),
)
run_crew_module._run_json_crew_in_project_env(
crew_path=tmp_path / "crew.jsonc", inputs='{"topic":"AI"}'
)
env = captured_kwargs[0]["env"]
assert env[run_crew_module._CREWAI_JSON_CREW_INPUTS_ENV] == '{"topic":"AI"}'
def test_run_json_crew_in_project_env_rejects_invalid_inputs_json(
monkeypatch, tmp_path: Path, capsys
):
monkeypatch.chdir(tmp_path)
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
monkeypatch.setattr(
run_crew_module.subprocess,
"run",
lambda *a, **k: pytest.fail("subprocess must not run on invalid --inputs"),
)
with pytest.raises(SystemExit) as exc_info:
run_crew_module._run_json_crew_in_project_env(
crew_path=tmp_path / "crew.jsonc", inputs="not json"
)
assert exc_info.value.code == 1
assert "Invalid --inputs JSON" in capsys.readouterr().err
def test_run_crew_forwards_inputs_to_json_crew(monkeypatch):
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
monkeypatch.setattr(
run_crew_module,
"configured_project_json_crew",
lambda *a, **k: Path("crew.jsonc"),
)
called: dict = {}
monkeypatch.setattr(
run_crew_module,
"_run_json_crew_in_project_env",
lambda **kw: called.update(kw),
)
run_crew_module.run_crew(inputs='{"topic":"AI"}')
assert called == {
"trained_agents_file": None,
"crew_path": Path("crew.jsonc"),
"inputs": '{"topic":"AI"}',
}
def test_configured_project_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.configured_project_json_crew() is None
def test_configured_project_json_crew_returns_declared_crew_definition(
monkeypatch, tmp_path: Path
):
monkeypatch.chdir(tmp_path)
crew_path = tmp_path / "crew.jsonc"
crew_path.write_text("{}")
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
)
assert run_crew_module.configured_project_json_crew() == crew_path.resolve()
def test_configured_project_json_crew_ignores_declared_crew_without_definition(
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.configured_project_json_crew() is None
def test_configured_project_json_crew_ignores_missing_pyproject(
monkeypatch, tmp_path: Path
):
monkeypatch.chdir(tmp_path)
(tmp_path / "crew.jsonc").write_text("{}")
assert run_crew_module.configured_project_json_crew() is None
def test_run_crew_inputs_rejected_for_classic_crew(monkeypatch):
# --inputs works for declarative flows and declarative (JSON) crews, but a
# classic crew takes its inputs from main.py, so it errors clearly.
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
monkeypatch.setattr(
run_crew_module, "configured_project_json_crew", lambda *a, **k: None
)
monkeypatch.setattr(
run_crew_module, "_warn_if_old_poetry_project", lambda *a, **k: None
)
monkeypatch.setattr(run_crew_module, "get_crewai_project_type", lambda *a, **k: "crew")
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(inputs='{"topic":"AI"}')
assert (
"--inputs is only supported for declarative flows and crews"
in exc_info.value.message
)
def test_run_crew_inputs_without_definition_resolves_configured_flow(monkeypatch):
# --inputs with no --definition resolves the configured [tool.crewai] flow,
# exactly like a bare `crewai run`, and forwards the inputs.
import crewai_cli.run_declarative_flow as rdf
calls: dict[str, object] = {}
monkeypatch.setattr(run_crew_module, "read_toml", lambda *a, **k: {})
monkeypatch.setattr(
run_crew_module, "configured_project_json_crew", lambda *a, **k: None
)
monkeypatch.setattr(
run_crew_module, "_warn_if_old_poetry_project", lambda *a, **k: None
)
monkeypatch.setattr(run_crew_module, "get_crewai_project_type", lambda *a, **k: "flow")
monkeypatch.setattr(
rdf, "configured_project_declarative_flow", lambda *a, **k: Path("flow.yaml")
)
monkeypatch.setattr(
rdf, "run_declarative_flow_in_project_env", lambda **kw: calls.update(kw)
)
run_crew_module.run_crew(inputs='{"topic":"AI"}')
assert calls == {"definition": Path("flow.yaml"), "inputs": '{"topic":"AI"}'}
def test_run_crew_rejects_filename_with_explicit_definition():
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(
trained_agents_file="trained.pkl",
definition="flow.yaml",
)
assert "--filename can only be used when running crews" in exc_info.value.message
def test_run_crew_runs_explicit_declarative_definition(monkeypatch, capsys):
calls = []
def fake_run_declarative_flow(definition: str, inputs: str | None = None):
calls.append((definition, inputs))
monkeypatch.setattr(
"crewai_cli.run_declarative_flow.run_declarative_flow",
fake_run_declarative_flow,
)
run_crew_module.run_crew(definition="flow.yaml", inputs='{"topic":"AI"}')
captured = capsys.readouterr()
assert "experimental" not in captured.out.lower()
assert calls == [("flow.yaml", '{"topic":"AI"}')]
def test_run_crew_runs_classic_crew_project(monkeypatch, capsys):
calls = []
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "crew"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
)
run_crew_module.run_crew(trained_agents_file="trained.pkl")
assert capsys.readouterr().out == ""
assert calls == [
(
"run_crew",
{"entity_type": "crew", "trained_agents_file": "trained.pkl"},
)
]
def test_run_crew_runs_python_flow_project(monkeypatch, capsys):
calls = []
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda script_name, **kwargs: calls.append((script_name, kwargs)),
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
lambda: None,
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [("kickoff", {"entity_type": "flow"})]
def test_run_crew_runs_conversational_flow_tui(monkeypatch, capsys):
class Flow:
pass
flow = Flow()
calls = []
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._load_conversational_flow_from_kickoff_script",
lambda: flow,
)
monkeypatch.setattr(
"crewai_cli.kickoff_flow._run_conversational_flow_tui",
lambda loaded_flow: calls.append(loaded_flow),
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail(
"conversational flows must use the TUI"
),
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [flow]
def test_run_crew_rejects_filename_for_flow_project(monkeypatch):
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {"tool": {"crewai": {"type": "flow"}}},
)
with pytest.raises(click.UsageError) as exc_info:
run_crew_module.run_crew(trained_agents_file="trained.pkl")
assert "--filename can only be used when running crews" in exc_info.value.message
def test_run_crew_runs_configured_declarative_flow_project(
monkeypatch, tmp_path: Path, capsys
):
calls = []
monkeypatch.chdir(tmp_path)
definition_path = tmp_path / "flow.yaml"
definition_path.write_text("schema: crewai.flow/v1\n", encoding="utf-8")
monkeypatch.setattr(
run_crew_module,
"read_toml",
lambda: {
"tool": {
"crewai": {
"type": "flow",
"definition": "flow.yaml",
}
}
},
)
monkeypatch.setattr(
"crewai_cli.run_declarative_flow.run_declarative_flow_in_project_env",
lambda definition, inputs=None: calls.append((definition, inputs)),
)
monkeypatch.setattr(
run_crew_module,
"_execute_uv_script",
lambda *_args, **_kwargs: pytest.fail("declarative flows must not run kickoff"),
)
run_crew_module.run_crew()
assert capsys.readouterr().out == ""
assert calls == [(definition_path.resolve(), None)]