Files
crewAI/lib/crewai/tests/cli/test_cli.py
Vinicius Brasil 596150188b Require explicit CrewAI project definitions (#6358)
* Require explicit CrewAI project definitions

JSON crews and declarative flows now resolve from `[tool.crewai]`
metadata instead of implicit filename discovery. This makes project type
selection deterministic, prevents stray `crew.json(c)` files from changing
CLI behavior, and centralizes definition path validation for run, install,
deploy validation, plotting, and memory reset paths.

`[tool.crewai].definition` must be a project-local file path. Absolute
paths, `~`, missing files, directories, and paths escaping the project root
are rejected so deploy and runtime commands use the same contract.

Breaking changes and migration paths:

* JSON crew projects are no longer discovered from `crew.json` or
  `crew.jsonc` alone. Add explicit metadata:

  ```toml
  [tool.crewai]
  type = "crew"
  definition = "crew.jsonc"
  ```

* Declarative flow projects must use a valid project-local definition path:

  ```toml
  [tool.crewai]
  type = "flow"
  definition = "flows/research.yaml"
  ```

* `Flow.from_definition(definition)` is removed. Use:

  ```python
  Flow.from_declaration(contents=definition)
  ```

* `FlowDefinition.to_json()` and `FlowDefinition.to_yaml()` are removed.
  Use `FlowDefinition.to_dict()` and serialize with the caller's JSON or
  YAML library.

* `FlowDefinition.from_dict()` is removed. Use:

  ```python
  FlowDefinition.from_declaration(contents=data)
  ```

* `FlowDefinition.json_schema()` is removed. Use Pydantic's schema API only
  where schema generation is intentionally needed:

  ```python
  FlowDefinition.model_json_schema(by_alias=True)
  ```

* `crewai_cli.run_crew.find_crew_json_file()` and `_has_json_crew()` are
  removed. Use `configured_project_json_crew()` or the shared
  `crewai_core.project.configured_project_definition("crew")` helper.

* `crewai reset-memories` now only loads JSON crews declared through
  `[tool.crewai].definition`, and invalid declared JSON crew definitions
  fail instead of silently falling back to classic crew discovery.

* Address code review comments
2026-06-26 12:07:03 -07:00

347 lines
12 KiB
Python

"""Tests for CLI commands that require crewai core (reset-memories).
Non-core CLI tests (train, test, version, deploy, login, flow_add_crew)
have moved to lib/cli/tests/test_cli.py.
"""
from pathlib import Path
from unittest import mock
from click.testing import CliRunner
from crewai.crew import Crew
from crewai.memory.unified_memory import Memory
from crewai_cli.cli import reset_memories
import pytest
@pytest.fixture
def runner():
return CliRunner()
@pytest.fixture
def mock_crew():
_mock = mock.Mock(spec=Crew, name="test_crew")
_mock.name = "test_crew"
return _mock
@pytest.fixture
def mock_get_crews(mock_crew):
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[mock_crew]
) as mock_get_crew, mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
):
yield mock_get_crew
def test_reset_all_memories(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-a"])
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="all")
assert (
f"[Crew ({crew.name})] Reset memories command has been completed."
in result.output
)
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_memory(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-m"])
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="memory")
assert (
f"[Crew ({crew.name})] Memory has been reset." in result.output
)
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_short_flag_deprecated_maps_to_memory(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-s"])
assert "deprecated" in result.output.lower()
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="memory")
assert f"[Crew ({crew.name})] Memory has been reset." in result.output
def test_reset_entity_flag_deprecated_maps_to_memory(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-e"])
assert "deprecated" in result.output.lower()
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="memory")
assert f"[Crew ({crew.name})] Memory has been reset." in result.output
def test_reset_long_flag_deprecated_maps_to_memory(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-l"])
assert "deprecated" in result.output.lower()
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="memory")
assert f"[Crew ({crew.name})] Memory has been reset." in result.output
def test_reset_kickoff_outputs(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-k"])
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="kickoff_outputs")
assert (
f"[Crew ({crew.name})] Latest Kickoff outputs stored has been reset."
in result.output
)
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_multiple_legacy_flags_collapsed_to_single_memory_reset(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["-s", "-l"])
assert "deprecated" in result.output.lower()
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="memory")
assert f"[Crew ({crew.name})] Memory has been reset." in result.output
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_knowledge(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["--knowledge"])
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="knowledge")
assert f"[Crew ({crew.name})] Knowledge has been reset." in result.output
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_agent_knowledge(mock_get_crews, runner):
result = runner.invoke(reset_memories, ["--agent-knowledge"])
call_count = 0
for crew in mock_get_crews.return_value:
crew.reset_memories.assert_called_once_with(command_type="agent_knowledge")
assert f"[Crew ({crew.name})] Agents knowledge has been reset." in result.output
call_count += 1
assert call_count == 1, "reset_memories should have been called once"
def test_reset_memory_from_many_crews(mock_get_crews, runner):
crews = []
for crew_id in ["id-1234", "id-5678"]:
mock_crew = mock.Mock(spec=Crew)
mock_crew.name = None
mock_crew.id = crew_id
crews.append(mock_crew)
mock_get_crews.return_value = crews
result = runner.invoke(reset_memories, ["--knowledge"])
call_count = 0
for crew in crews:
call_count += 1
crew.reset_memories.assert_called_once_with(command_type="knowledge")
assert f"[Crew ({crew.id})] Knowledge has been reset." in result.output
assert call_count == 2, "reset_memories should have been called twice"
@pytest.fixture
def mock_flow():
_mock = mock.Mock()
_mock.name = "TestFlow"
_mock.memory = mock.Mock()
_mock.memory.reset = mock.Mock()
return _mock
@pytest.fixture
def mock_get_flows(mock_flow):
with mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[mock_flow]
) as mock_get_flow, mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
):
yield mock_get_flow
def test_reset_flow_memory(mock_get_flows, mock_flow, runner):
result = runner.invoke(reset_memories, ["-m"])
mock_flow.memory.reset.assert_called_once()
assert "[Flow (TestFlow)] Memory has been reset." in result.output
def test_reset_flow_unified_memory_uses_full_reset(runner, tmp_path):
flow = mock.Mock()
flow.name = "TestFlow"
flow.memory = Memory(
storage=str(tmp_path / "db"),
llm=mock.Mock(),
embedder=lambda texts: [[0.1] * 4 for _ in texts],
)
with mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[flow]
), mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
), mock.patch.object(
Memory, "reset_all"
) as reset_all, mock.patch.object(
Memory, "reset"
) as reset:
result = runner.invoke(reset_memories, ["-m"])
reset_all.assert_called_once_with()
reset.assert_not_called()
assert "[Flow (TestFlow)] Memory has been reset." in result.output
def test_reset_flow_all_memories(mock_get_flows, mock_flow, runner):
result = runner.invoke(reset_memories, ["-a"])
mock_flow.memory.reset.assert_called_once()
assert "[Flow (TestFlow)] Reset memories command has been completed." in result.output
def test_reset_flow_knowledge_no_effect(mock_get_flows, mock_flow, runner):
result = runner.invoke(reset_memories, ["--knowledge"])
mock_flow.memory.reset.assert_not_called()
assert "[Flow (TestFlow)]" not in result.output
def test_reset_no_crew_or_flow_found(runner):
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
):
result = runner.invoke(reset_memories, ["-m"])
assert "No crew or flow found." in result.output
def test_reset_json_crew_memory(mock_crew, runner, monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
(tmp_path / "crew.jsonc").write_text("{}")
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
)
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.load_crew",
return_value=(mock_crew, {}),
) as mock_load_crew:
result = runner.invoke(reset_memories, ["-m"])
mock_load_crew.assert_called_once_with((tmp_path / "crew.jsonc").resolve())
mock_crew.reset_memories.assert_called_once_with(command_type="memory")
assert f"[Crew ({mock_crew.name})] Memory has been reset." in result.output
def test_reset_invalid_json_crew_blocks_reset(
mock_crew, runner, monkeypatch, tmp_path
):
monkeypatch.chdir(tmp_path)
(tmp_path / "crew.jsonc").write_text("{invalid")
(tmp_path / "pyproject.toml").write_text(
'[tool.crewai]\ntype = "crew"\ndefinition = "crew.jsonc"\n'
)
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[mock_crew]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.load_crew",
side_effect=ValueError("invalid JSON"),
) as mock_load_crew:
result = runner.invoke(reset_memories, ["-m"])
mock_load_crew.assert_called_once_with((tmp_path / "crew.jsonc").resolve())
mock_crew.reset_memories.assert_not_called()
assert result.exit_code != 0
assert "An unexpected error occurred: invalid JSON" in result.output
def test_reset_json_crew_skipped_for_declared_flow_project(
mock_crew, runner, monkeypatch, tmp_path
):
monkeypatch.chdir(tmp_path)
(tmp_path / "crew.jsonc").write_text("{}")
(tmp_path / "pyproject.toml").write_text('[tool.crewai]\ntype = "flow"\n')
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.load_crew",
return_value=(mock_crew, {}),
) as mock_load_crew:
result = runner.invoke(reset_memories, ["-m"])
mock_load_crew.assert_not_called()
mock_crew.reset_memories.assert_not_called()
assert "No crew or flow found." in result.output
def test_reset_crew_and_flow_memory(mock_crew, mock_flow, runner):
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[mock_crew]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[mock_flow]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
):
result = runner.invoke(reset_memories, ["-m"])
mock_crew.reset_memories.assert_called_once_with(command_type="memory")
mock_flow.memory.reset.assert_called_once()
assert f"[Crew ({mock_crew.name})] Memory has been reset." in result.output
assert "[Flow (TestFlow)] Memory has been reset." in result.output
def test_reset_flow_memory_none(runner):
mock_flow = mock.Mock()
mock_flow.name = "NoMemFlow"
mock_flow.memory = None
with mock.patch(
"crewai.utilities.reset_memories.get_crews", return_value=[]
), mock.patch(
"crewai.utilities.reset_memories.get_flows", return_value=[mock_flow]
), mock.patch(
"crewai.utilities.reset_memories._get_json_crew", return_value=None
):
result = runner.invoke(reset_memories, ["-m"])
assert "[Flow (NoMemFlow)] Memory has been reset." in result.output
def test_reset_no_memory_flags(runner):
result = runner.invoke(
reset_memories,
)
assert (
result.output
== "Please specify at least one memory type to reset using the appropriate flags.\n"
)