1- from unittest .mock import patch
1+ """Tests for the planning handler module."""
2+
3+ from unittest .mock import MagicMock , patch
24
35import pytest
6+
47from crewai .agent import Agent
8+ from crewai .crew import Crew
59from crewai .knowledge .source .string_knowledge_source import StringKnowledgeSource
610from crewai .task import Task
711from crewai .tasks .task_output import TaskOutput
1317)
1418
1519
16- class InternalCrewPlanner :
20+ class TestInternalCrewPlanner :
1721 @pytest .fixture
1822 def crew_planner (self ):
1923 tasks = [
@@ -49,9 +53,9 @@ def crew_planner_different_llm(self):
4953
5054 def test_handle_crew_planning (self , crew_planner ):
5155 list_of_plans_per_task = [
52- PlanPerTask (task = "Task1" , plan = "Plan 1" ),
53- PlanPerTask (task = "Task2" , plan = "Plan 2" ),
54- PlanPerTask (task = "Task3" , plan = "Plan 3" ),
56+ PlanPerTask (task_number = 1 , task = "Task1" , plan = "Plan 1" ),
57+ PlanPerTask (task_number = 2 , task = "Task2" , plan = "Plan 2" ),
58+ PlanPerTask (task_number = 3 , task = "Task3" , plan = "Plan 3" ),
5559 ]
5660 with patch .object (Task , "execute_sync" ) as execute :
5761 execute .return_value = TaskOutput (
@@ -97,12 +101,12 @@ def test_create_tasks_summary(self, crew_planner):
97101 # Knowledge field should not be present when empty
98102 assert '"agent_knowledge"' not in tasks_summary
99103
100- @patch ("crewai.knowledge.storage.knowledge_storage.chromadb" )
101- def test_create_tasks_summary_with_knowledge_and_tools (self , mock_chroma ):
104+ @patch ("crewai.knowledge.knowledge.Knowledge.add_sources" )
105+ @patch ("crewai.knowledge.storage.knowledge_storage.KnowledgeStorage" )
106+ def test_create_tasks_summary_with_knowledge_and_tools (
107+ self , mock_storage , mock_add_sources
108+ ):
102109 """Test task summary generation with both knowledge and tools present."""
103- # Mock ChromaDB collection
104- mock_collection = mock_chroma .return_value .get_or_create_collection .return_value
105- mock_collection .add .return_value = None
106110
107111 # Create mock tools with proper string descriptions and structured tool support
108112 class MockTool (BaseTool ):
@@ -166,7 +170,9 @@ def test_handle_crew_planning_different_llm(self, crew_planner_different_llm):
166170 description = "Description" ,
167171 agent = "agent" ,
168172 pydantic = PlannerTaskPydanticOutput (
169- list_of_plans_per_task = [PlanPerTask (task = "Task1" , plan = "Plan 1" )]
173+ list_of_plans_per_task = [
174+ PlanPerTask (task_number = 1 , task = "Task1" , plan = "Plan 1" )
175+ ]
170176 ),
171177 )
172178 result = crew_planner_different_llm ._handle_crew_planning ()
@@ -177,3 +183,181 @@ def test_handle_crew_planning_different_llm(self, crew_planner_different_llm):
177183 crew_planner_different_llm .tasks
178184 )
179185 execute .assert_called_once ()
186+
187+ def test_plan_per_task_requires_task_number (self ):
188+ """Test that PlanPerTask model requires task_number field."""
189+ with pytest .raises (ValueError ):
190+ PlanPerTask (task = "Task1" , plan = "Plan 1" )
191+
192+ def test_plan_per_task_with_task_number (self ):
193+ """Test PlanPerTask model with task_number field."""
194+ plan = PlanPerTask (task_number = 5 , task = "Task5" , plan = "Plan for task 5" )
195+ assert plan .task_number == 5
196+ assert plan .task == "Task5"
197+ assert plan .plan == "Plan for task 5"
198+
199+
200+ class TestCrewPlanningIntegration :
201+ """Tests for Crew._handle_crew_planning integration with task_number matching."""
202+
203+ def test_crew_planning_with_out_of_order_plans (self ):
204+ """Test that plans are correctly matched to tasks even when returned out of order.
205+
206+ This test verifies the fix for issue #3953 where plans returned by the LLM
207+ in a different order than the tasks would be incorrectly assigned.
208+ """
209+ agent1 = Agent (role = "Agent 1" , goal = "Goal 1" , backstory = "Backstory 1" )
210+ agent2 = Agent (role = "Agent 2" , goal = "Goal 2" , backstory = "Backstory 2" )
211+ agent3 = Agent (role = "Agent 3" , goal = "Goal 3" , backstory = "Backstory 3" )
212+
213+ task1 = Task (
214+ description = "First task description" ,
215+ expected_output = "Output 1" ,
216+ agent = agent1 ,
217+ )
218+ task2 = Task (
219+ description = "Second task description" ,
220+ expected_output = "Output 2" ,
221+ agent = agent2 ,
222+ )
223+ task3 = Task (
224+ description = "Third task description" ,
225+ expected_output = "Output 3" ,
226+ agent = agent3 ,
227+ )
228+
229+ crew = Crew (
230+ agents = [agent1 , agent2 , agent3 ],
231+ tasks = [task1 , task2 , task3 ],
232+ planning = True ,
233+ )
234+
235+ out_of_order_plans = [
236+ PlanPerTask (task_number = 3 , task = "Task 3" , plan = " [PLAN FOR TASK 3]" ),
237+ PlanPerTask (task_number = 1 , task = "Task 1" , plan = " [PLAN FOR TASK 1]" ),
238+ PlanPerTask (task_number = 2 , task = "Task 2" , plan = " [PLAN FOR TASK 2]" ),
239+ ]
240+
241+ mock_planner_result = PlannerTaskPydanticOutput (
242+ list_of_plans_per_task = out_of_order_plans
243+ )
244+
245+ with patch .object (
246+ CrewPlanner , "_handle_crew_planning" , return_value = mock_planner_result
247+ ):
248+ crew ._handle_crew_planning ()
249+
250+ assert "[PLAN FOR TASK 1]" in task1 .description
251+ assert "[PLAN FOR TASK 2]" in task2 .description
252+ assert "[PLAN FOR TASK 3]" in task3 .description
253+
254+ assert "[PLAN FOR TASK 3]" not in task1 .description
255+ assert "[PLAN FOR TASK 1]" not in task2 .description
256+ assert "[PLAN FOR TASK 2]" not in task3 .description
257+
258+ def test_crew_planning_with_missing_plan (self ):
259+ """Test that missing plans are handled gracefully with a warning."""
260+ agent1 = Agent (role = "Agent 1" , goal = "Goal 1" , backstory = "Backstory 1" )
261+ agent2 = Agent (role = "Agent 2" , goal = "Goal 2" , backstory = "Backstory 2" )
262+
263+ task1 = Task (
264+ description = "First task description" ,
265+ expected_output = "Output 1" ,
266+ agent = agent1 ,
267+ )
268+ task2 = Task (
269+ description = "Second task description" ,
270+ expected_output = "Output 2" ,
271+ agent = agent2 ,
272+ )
273+
274+ crew = Crew (
275+ agents = [agent1 , agent2 ],
276+ tasks = [task1 , task2 ],
277+ planning = True ,
278+ )
279+
280+ original_task1_desc = task1 .description
281+ original_task2_desc = task2 .description
282+
283+ incomplete_plans = [
284+ PlanPerTask (task_number = 1 , task = "Task 1" , plan = " [PLAN FOR TASK 1]" ),
285+ ]
286+
287+ mock_planner_result = PlannerTaskPydanticOutput (
288+ list_of_plans_per_task = incomplete_plans
289+ )
290+
291+ with patch .object (
292+ CrewPlanner , "_handle_crew_planning" , return_value = mock_planner_result
293+ ):
294+ crew ._handle_crew_planning ()
295+
296+ assert "[PLAN FOR TASK 1]" in task1 .description
297+
298+ assert task2 .description == original_task2_desc
299+
300+ def test_crew_planning_preserves_original_description (self ):
301+ """Test that planning appends to the original task description."""
302+ agent = Agent (role = "Agent 1" , goal = "Goal 1" , backstory = "Backstory 1" )
303+
304+ task = Task (
305+ description = "Original task description" ,
306+ expected_output = "Output 1" ,
307+ agent = agent ,
308+ )
309+
310+ crew = Crew (
311+ agents = [agent ],
312+ tasks = [task ],
313+ planning = True ,
314+ )
315+
316+ plans = [
317+ PlanPerTask (task_number = 1 , task = "Task 1" , plan = " - Additional plan steps" ),
318+ ]
319+
320+ mock_planner_result = PlannerTaskPydanticOutput (list_of_plans_per_task = plans )
321+
322+ with patch .object (
323+ CrewPlanner , "_handle_crew_planning" , return_value = mock_planner_result
324+ ):
325+ crew ._handle_crew_planning ()
326+
327+ assert "Original task description" in task .description
328+ assert "Additional plan steps" in task .description
329+
330+ def test_crew_planning_with_duplicate_task_numbers (self ):
331+ """Test that duplicate task numbers use the first plan and log a warning."""
332+ agent = Agent (role = "Agent 1" , goal = "Goal 1" , backstory = "Backstory 1" )
333+
334+ task = Task (
335+ description = "Task description" ,
336+ expected_output = "Output 1" ,
337+ agent = agent ,
338+ )
339+
340+ crew = Crew (
341+ agents = [agent ],
342+ tasks = [task ],
343+ planning = True ,
344+ )
345+
346+ # Two plans with the same task_number - should use the first one
347+ duplicate_plans = [
348+ PlanPerTask (task_number = 1 , task = "Task 1" , plan = " [FIRST PLAN]" ),
349+ PlanPerTask (task_number = 1 , task = "Task 1" , plan = " [SECOND PLAN]" ),
350+ ]
351+
352+ mock_planner_result = PlannerTaskPydanticOutput (
353+ list_of_plans_per_task = duplicate_plans
354+ )
355+
356+ with patch .object (
357+ CrewPlanner , "_handle_crew_planning" , return_value = mock_planner_result
358+ ):
359+ crew ._handle_crew_planning ()
360+
361+ # Should use the first plan, not the second
362+ assert "[FIRST PLAN]" in task .description
363+ assert "[SECOND PLAN]" not in task .description
0 commit comments