mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-23 15:55:11 +00:00
Add declarative Flow CLI support
Currently, declarative flows can be loaded by the runtime, but the CLI still treats them as an experimental definition file instead of a first-class Flow project shape. With this PR, `crewai create flow --declarative` scaffolds a YAML-backed Flow project, and `crewai run`, `crewai flow kickoff`, and `crewai flow plot` can run against the configured definition. This also lets crew actions reference reusable crew definition files or folders and override their inputs from the Flow definition, so declarative flows can compose existing declarative crews without inlining everything.
This commit is contained in:
@@ -130,31 +130,49 @@ def test_run_uses_project_runner_by_default(run_crew, runner):
|
||||
assert "experimental" not in result.output.lower()
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_flow_definition")
|
||||
def test_run_with_definition_uses_definition_runner(run_flow_definition, runner):
|
||||
@mock.patch("crewai_cli.cli.run_declarative_flow_in_project_env")
|
||||
def test_run_with_definition_uses_project_env_declarative_runner(
|
||||
run_declarative_flow_in_project_env, runner, monkeypatch
|
||||
):
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
result = runner.invoke(
|
||||
run,
|
||||
["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
"Warning: `crewai run --definition` is experimental and may change without notice."
|
||||
in result.output
|
||||
assert "experimental" not in result.output.lower()
|
||||
run_declarative_flow_in_project_env.assert_called_once_with(
|
||||
definition="flow.yaml", inputs='{"topic":"AI"}'
|
||||
)
|
||||
run_flow_definition.assert_called_once_with(
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_declarative_flow_in_project_env")
|
||||
def test_run_with_definition_delegates_project_env_detection_to_runner(
|
||||
run_declarative_flow_in_project_env, runner
|
||||
):
|
||||
result = runner.invoke(
|
||||
run,
|
||||
["--definition", "flow.yaml", "--inputs", '{"topic":"AI"}'],
|
||||
env={"UV_RUN_RECURSION_DEPTH": "1"},
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
run_declarative_flow_in_project_env.assert_called_once_with(
|
||||
definition="flow.yaml", inputs='{"topic":"AI"}'
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.cli.run_crew")
|
||||
@mock.patch("crewai_cli.cli.run_flow_definition")
|
||||
def test_run_rejects_inputs_without_definition(run_flow_definition, run_crew, runner):
|
||||
@mock.patch("crewai_cli.cli.run_declarative_flow_in_project_env")
|
||||
def test_run_rejects_inputs_without_definition(
|
||||
run_declarative_flow_in_project_env, run_crew, runner
|
||||
):
|
||||
result = runner.invoke(run, ["--inputs", '{"topic":"AI"}'])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "Error: --inputs requires --definition" in result.output
|
||||
run_flow_definition.assert_not_called()
|
||||
run_declarative_flow_in_project_env.assert_not_called()
|
||||
run_crew.assert_not_called()
|
||||
|
||||
|
||||
@@ -166,6 +184,23 @@ def test_create_crew_in_dmn_mode_skips_provider_prompts(create_json_crew, runner
|
||||
create_json_crew.assert_called_once_with("DMN Crew", None, True)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_flow.create_flow")
|
||||
def test_create_flow_declarative_uses_declarative_scaffold(create_flow, runner):
|
||||
result = runner.invoke(create, ["flow", "My Flow", "--declarative"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
create_flow.assert_called_once_with("My Flow", declarative=True)
|
||||
|
||||
|
||||
@mock.patch("crewai_cli.create_json_crew.create_json_crew")
|
||||
def test_create_crew_rejects_declarative_flag(create_json_crew, runner):
|
||||
result = runner.invoke(create, ["crew", "My Crew", "--declarative"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "--declarative can only be used with flow projects" in result.output
|
||||
create_json_crew.assert_not_called()
|
||||
|
||||
|
||||
def test_create_requires_type_in_dmn_mode(runner):
|
||||
result = runner.invoke(create, env={"CREWAI_DMN": "True"})
|
||||
|
||||
|
||||
31
lib/cli/tests/test_create_flow.py
Normal file
31
lib/cli/tests/test_create_flow.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from crewai_cli.create_flow import create_flow
|
||||
|
||||
|
||||
def test_create_flow_declarative_scaffold(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
create_flow("Research Flow", declarative=True)
|
||||
|
||||
project_root = tmp_path / "research_flow"
|
||||
assert (project_root / "flow.yaml").is_file()
|
||||
assert (project_root / "crews").is_dir()
|
||||
assert (project_root / "tools").is_dir()
|
||||
assert (project_root / "knowledge").is_dir()
|
||||
assert (project_root / "skills").is_dir()
|
||||
assert not (project_root / "src").exists()
|
||||
|
||||
pyproject = (project_root / "pyproject.toml").read_text(encoding="utf-8")
|
||||
assert 'type = "flow"' in pyproject
|
||||
assert 'definition = "flow.yaml"' in pyproject
|
||||
assert (
|
||||
'only-include = ["flow.yaml", "crews", "tools", "knowledge", "skills"]'
|
||||
in pyproject
|
||||
)
|
||||
|
||||
flow_definition = (project_root / "flow.yaml").read_text(encoding="utf-8")
|
||||
assert "schema: crewai.flow/v1" in flow_definition
|
||||
assert "name: ResearchFlow" in flow_definition
|
||||
139
lib/cli/tests/test_flow_commands.py
Normal file
139
lib/cli/tests/test_flow_commands.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import crewai_cli.kickoff_flow as kickoff_flow_module
|
||||
import crewai_cli.plot_flow as plot_flow_module
|
||||
|
||||
|
||||
def test_kickoff_flow_uses_configured_definition(monkeypatch):
|
||||
calls = []
|
||||
subprocess_calls = []
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.configured_project_declarative_flow",
|
||||
lambda: "flow.yaml",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.run_declarative_flow",
|
||||
lambda **kwargs: calls.append({"in_process": kwargs}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.run_declarative_flow_in_project_env",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
kickoff_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
kickoff_flow_module.kickoff_flow()
|
||||
|
||||
assert calls == [{"definition": "flow.yaml"}]
|
||||
assert subprocess_calls == []
|
||||
|
||||
|
||||
def test_kickoff_flow_keeps_python_entrypoint_without_definition(monkeypatch):
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.configured_project_declarative_flow",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
kickoff_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
kickoff_flow_module.kickoff_flow()
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
(["uv", "run", "kickoff"],),
|
||||
{"capture_output": False, "text": True, "check": True},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_plot_flow_uses_configured_definition(monkeypatch):
|
||||
calls = []
|
||||
subprocess_calls = []
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.configured_project_declarative_flow",
|
||||
lambda: "flow.yaml",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.plot_declarative_flow",
|
||||
lambda definition: calls.append({"in_process": definition}),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.plot_declarative_flow_in_project_env",
|
||||
lambda definition: calls.append(definition),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plot_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
plot_flow_module.plot_flow()
|
||||
|
||||
assert calls == ["flow.yaml"]
|
||||
assert subprocess_calls == []
|
||||
|
||||
|
||||
def test_plot_flow_delegates_project_env_detection_to_runner(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.configured_project_declarative_flow",
|
||||
lambda: "flow.yaml",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.plot_declarative_flow_in_project_env",
|
||||
lambda definition: calls.append({"project_env": definition}),
|
||||
)
|
||||
|
||||
plot_flow_module.plot_flow()
|
||||
|
||||
assert calls == [{"project_env": "flow.yaml"}]
|
||||
|
||||
|
||||
def test_plot_flow_keeps_python_entrypoint_without_definition(monkeypatch):
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai_cli.run_declarative_flow.configured_project_declarative_flow",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
plot_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
plot_flow_module.plot_flow()
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
(["uv", "run", "plot"],),
|
||||
{"capture_output": False, "text": True, "check": True},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_configured_project_declarative_flow(monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
'[tool.crewai]\ntype = "flow"\ndefinition = "flow.yaml"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from crewai_cli.run_declarative_flow import configured_project_declarative_flow
|
||||
|
||||
assert configured_project_declarative_flow() == "flow.yaml"
|
||||
@@ -51,6 +51,63 @@ def test_run_crew_forwards_trained_agents_file_to_json_crews(monkeypatch):
|
||||
assert called == {"trained_agents_file": "some.pkl"}
|
||||
|
||||
|
||||
def test_run_crew_uses_configured_declarative_flow(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(run_crew_module, "get_crewai_version", lambda: "999.0.0")
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "flow", "definition": "flow.yaml"}}},
|
||||
)
|
||||
execute_calls = []
|
||||
declarative_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"execute_command",
|
||||
lambda *args, **kwargs: execute_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
import crewai_cli.run_declarative_flow as run_declarative_flow_module
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"run_declarative_flow_in_project_env",
|
||||
lambda **kwargs: declarative_calls.append(kwargs),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew()
|
||||
|
||||
assert declarative_calls == [{"definition": "flow.yaml"}]
|
||||
assert execute_calls == []
|
||||
|
||||
|
||||
def test_run_crew_without_definition_keeps_python_flow_runner(monkeypatch):
|
||||
monkeypatch.setattr(run_crew_module, "_has_json_crew", lambda: False)
|
||||
monkeypatch.setattr(run_crew_module, "get_crewai_version", lambda: "999.0.0")
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"read_toml",
|
||||
lambda: {"tool": {"crewai": {"type": "flow"}}},
|
||||
)
|
||||
execute_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_crew_module,
|
||||
"execute_command",
|
||||
lambda *args, **kwargs: execute_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
run_crew_module.run_crew(trained_agents_file="trained.pkl")
|
||||
|
||||
assert execute_calls == [
|
||||
(
|
||||
(run_crew_module.CrewType.FLOW,),
|
||||
{"trained_agents_file": "trained.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)
|
||||
|
||||
301
lib/cli/tests/test_run_declarative_flow.py
Normal file
301
lib/cli/tests/test_run_declarative_flow.py
Normal file
@@ -0,0 +1,301 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
import crewai_cli.run_declarative_flow as run_declarative_flow_module
|
||||
from crewai_cli.run_declarative_flow import run_declarative_flow
|
||||
|
||||
|
||||
class _FakeFlow:
|
||||
def __init__(self, definition):
|
||||
self.definition = definition
|
||||
|
||||
def kickoff(self, inputs=None):
|
||||
return {
|
||||
"flow": self.definition["name"],
|
||||
"inputs": inputs or {},
|
||||
}
|
||||
|
||||
|
||||
class _FakeFlowFactory:
|
||||
@classmethod
|
||||
def from_definition(cls, definition):
|
||||
return _FakeFlow(definition)
|
||||
|
||||
|
||||
class _FakeFlowDefinition:
|
||||
@classmethod
|
||||
def from_yaml(cls, source, *, source_path):
|
||||
return yaml.safe_load(source)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, source, *, source_path):
|
||||
return json.loads(source)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_flow_runtime(monkeypatch):
|
||||
crewai_module = types.ModuleType("crewai")
|
||||
flow_package = types.ModuleType("crewai.flow")
|
||||
flow_module = types.ModuleType("crewai.flow.flow")
|
||||
flow_definition_module = types.ModuleType("crewai.flow.flow_definition")
|
||||
|
||||
flow_module.Flow = _FakeFlowFactory
|
||||
flow_definition_module.FlowDefinition = _FakeFlowDefinition
|
||||
|
||||
monkeypatch.setitem(sys.modules, "crewai", crewai_module)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow", flow_package)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module)
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "crewai.flow.flow_definition", flow_definition_module
|
||||
)
|
||||
|
||||
|
||||
def _captured_json(capsys):
|
||||
return json.loads(capsys.readouterr().out)
|
||||
|
||||
|
||||
def test_run_declarative_flow_reads_definition_file(
|
||||
tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
run_declarative_flow(str(definition_path), '{"topic":"AI"}')
|
||||
|
||||
assert _captured_json(capsys) == {
|
||||
"flow": "TestFlow",
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filename", "definition_source", "expected_flow_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"flow.yaml",
|
||||
"schema: crewai.flow/v1\nname: YamlFileFlow\n",
|
||||
"YamlFileFlow",
|
||||
id="yaml-file",
|
||||
),
|
||||
pytest.param(
|
||||
"flow.json",
|
||||
'{"schema":"crewai.flow/v1","name":"JsonFlow"}',
|
||||
"JsonFlow",
|
||||
id="json-file",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_run_declarative_flow_accepts_definition_files(
|
||||
filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / filename
|
||||
definition_path.write_text(definition_source)
|
||||
|
||||
run_declarative_flow(str(definition_path))
|
||||
|
||||
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
|
||||
|
||||
|
||||
def test_run_declarative_flow_rejects_non_object_inputs(
|
||||
tmp_path, fake_flow_runtime, capsys
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow(str(definition_path), '["not", "an", "object"]')
|
||||
|
||||
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_declarative_flow_reports_unreadable_file(
|
||||
monkeypatch, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
def raise_permission_error(self, *args, **kwargs):
|
||||
raise PermissionError("no access")
|
||||
|
||||
monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow(str(definition_path))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "Unable to read --definition path" in err
|
||||
assert str(definition_path) in err
|
||||
assert "no access" in err
|
||||
|
||||
|
||||
def test_run_declarative_flow_reports_missing_file(capsys, fake_flow_runtime):
|
||||
with pytest.raises(SystemExit):
|
||||
run_declarative_flow("missing-flow.yaml")
|
||||
|
||||
assert (
|
||||
"Invalid --definition path: missing-flow.yaml does not exist."
|
||||
in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_uses_uv(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {"EXISTING": "value"},
|
||||
)
|
||||
|
||||
def fake_subprocess_run(command, **kwargs):
|
||||
subprocess_calls.append((command, kwargs))
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess, "run", fake_subprocess_run
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env(
|
||||
"flow.yaml", '{"topic":"AI"}'
|
||||
)
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"crewai",
|
||||
"run",
|
||||
"--definition",
|
||||
"flow.yaml",
|
||||
"--inputs",
|
||||
'{"topic":"AI"}',
|
||||
],
|
||||
{
|
||||
"capture_output": False,
|
||||
"text": True,
|
||||
"check": True,
|
||||
"env": {"EXISTING": "value"},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_falls_back_without_pyproject(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
calls = []
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"run_declarative_flow",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env("flow.yaml")
|
||||
|
||||
assert calls == [{"definition": "flow.yaml", "inputs": None}]
|
||||
|
||||
|
||||
def test_run_declarative_flow_in_project_env_uses_in_process_runner_inside_uv(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
calls = []
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"run_declarative_flow",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.run_declarative_flow_in_project_env(
|
||||
"flow.yaml", '{"topic":"AI"}'
|
||||
)
|
||||
|
||||
assert calls == [{"definition": "flow.yaml", "inputs": '{"topic":"AI"}'}]
|
||||
assert subprocess_calls == []
|
||||
|
||||
|
||||
def test_plot_declarative_flow_in_project_env_uses_uv(monkeypatch, tmp_path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("UV_RUN_RECURSION_DEPTH", raising=False)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"build_env_with_all_tool_credentials",
|
||||
lambda: {},
|
||||
)
|
||||
|
||||
def fake_subprocess_run(command, **kwargs):
|
||||
subprocess_calls.append((command, kwargs))
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess, "run", fake_subprocess_run
|
||||
)
|
||||
|
||||
run_declarative_flow_module.plot_declarative_flow_in_project_env("flow.yaml")
|
||||
|
||||
assert subprocess_calls == [
|
||||
(
|
||||
[
|
||||
"uv",
|
||||
"run",
|
||||
"crewai",
|
||||
"flow",
|
||||
"plot",
|
||||
],
|
||||
{
|
||||
"capture_output": False,
|
||||
"text": True,
|
||||
"check": True,
|
||||
"env": {},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_plot_declarative_flow_in_project_env_uses_in_process_runner_inside_uv(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("UV_RUN_RECURSION_DEPTH", "1")
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
calls = []
|
||||
subprocess_calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module,
|
||||
"plot_declarative_flow",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
run_declarative_flow_module.subprocess,
|
||||
"run",
|
||||
lambda *args, **kwargs: subprocess_calls.append((args, kwargs)),
|
||||
)
|
||||
|
||||
run_declarative_flow_module.plot_declarative_flow_in_project_env("flow.yaml")
|
||||
|
||||
assert calls == [{"definition": "flow.yaml"}]
|
||||
assert subprocess_calls == []
|
||||
@@ -1,156 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from crewai_cli.run_flow_definition import run_flow_definition
|
||||
|
||||
|
||||
class _FakeFlow:
|
||||
def __init__(self, definition):
|
||||
self.definition = definition
|
||||
|
||||
def kickoff(self, inputs=None):
|
||||
return {
|
||||
"flow": self.definition["name"],
|
||||
"inputs": inputs or {},
|
||||
}
|
||||
|
||||
|
||||
class _FakeFlowFactory:
|
||||
@classmethod
|
||||
def from_definition(cls, definition):
|
||||
return _FakeFlow(definition)
|
||||
|
||||
|
||||
class _FakeFlowDefinition:
|
||||
@classmethod
|
||||
def from_yaml(cls, source):
|
||||
return yaml.safe_load(source)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, source):
|
||||
return json.loads(source)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_flow_runtime(monkeypatch):
|
||||
crewai_module = types.ModuleType("crewai")
|
||||
flow_package = types.ModuleType("crewai.flow")
|
||||
flow_module = types.ModuleType("crewai.flow.flow")
|
||||
flow_definition_module = types.ModuleType("crewai.flow.flow_definition")
|
||||
|
||||
flow_module.Flow = _FakeFlowFactory
|
||||
flow_definition_module.FlowDefinition = _FakeFlowDefinition
|
||||
|
||||
monkeypatch.setitem(sys.modules, "crewai", crewai_module)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow", flow_package)
|
||||
monkeypatch.setitem(sys.modules, "crewai.flow.flow", flow_module)
|
||||
monkeypatch.setitem(
|
||||
sys.modules, "crewai.flow.flow_definition", flow_definition_module
|
||||
)
|
||||
|
||||
|
||||
def _captured_json(capsys):
|
||||
return json.loads(capsys.readouterr().out)
|
||||
|
||||
|
||||
def test_run_flow_definition_reads_definition_file(
|
||||
tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
run_flow_definition(str(definition_path), '{"topic":"AI"}')
|
||||
|
||||
assert _captured_json(capsys) == {
|
||||
"flow": "TestFlow",
|
||||
"inputs": {"topic": "AI"},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("definition_source", "expected_flow_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"schema: crewai.flow/v1\nname: InlineFlow\n",
|
||||
"InlineFlow",
|
||||
id="inline-yaml",
|
||||
),
|
||||
pytest.param(
|
||||
'{"schema":"crewai.flow/v1","name":"InlineJsonFlow"}',
|
||||
"InlineJsonFlow",
|
||||
id="inline-json",
|
||||
),
|
||||
pytest.param(
|
||||
'{"schema":"crewai.flow/v1","name":"' + ("JsonFlow" * 500) + '"}',
|
||||
"JsonFlow" * 500,
|
||||
id="large-inline-json",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_run_flow_definition_accepts_inline_definitions(
|
||||
definition_source, expected_flow_name, capsys, fake_flow_runtime
|
||||
):
|
||||
run_flow_definition(definition_source)
|
||||
|
||||
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("filename", "definition_source", "expected_flow_name"),
|
||||
[
|
||||
pytest.param(
|
||||
"flow.yaml",
|
||||
"schema: crewai.flow/v1\nname: YamlFileFlow\n",
|
||||
"YamlFileFlow",
|
||||
id="yaml-file",
|
||||
),
|
||||
pytest.param(
|
||||
"flow.json",
|
||||
'{"schema":"crewai.flow/v1","name":"JsonFlow"}',
|
||||
"JsonFlow",
|
||||
id="json-file",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_run_flow_definition_accepts_definition_files(
|
||||
filename, definition_source, expected_flow_name, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / filename
|
||||
definition_path.write_text(definition_source)
|
||||
|
||||
run_flow_definition(str(definition_path))
|
||||
|
||||
assert _captured_json(capsys) == {"flow": expected_flow_name, "inputs": {}}
|
||||
|
||||
|
||||
def test_run_flow_definition_rejects_non_object_inputs(fake_flow_runtime, capsys):
|
||||
with pytest.raises(SystemExit):
|
||||
run_flow_definition("name: TestFlow", '["not", "an", "object"]')
|
||||
|
||||
assert "Invalid --inputs JSON: expected an object." in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_run_flow_definition_reports_unreadable_file(
|
||||
monkeypatch, tmp_path, capsys, fake_flow_runtime
|
||||
):
|
||||
definition_path = tmp_path / "flow.yaml"
|
||||
definition_path.write_text("schema: crewai.flow/v1\nname: TestFlow\n")
|
||||
|
||||
def raise_permission_error(self, *args, **kwargs):
|
||||
raise PermissionError("no access")
|
||||
|
||||
monkeypatch.setattr("pathlib.Path.read_text", raise_permission_error)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
run_flow_definition(str(definition_path))
|
||||
|
||||
err = capsys.readouterr().err
|
||||
assert "Unable to read --definition path" in err
|
||||
assert str(definition_path) in err
|
||||
assert "no access" in err
|
||||
Reference in New Issue
Block a user