@@ -41,18 +41,15 @@ def test_build_initial_dispatch_state_sets_pending_status():
4141 assert all (item ["job_run_id" ] is None for item in dispatch_state )
4242
4343
44- @pytest .mark .anyio
45- @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.asyncio.create_task" )
46- @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.asyncio.to_thread" , new_callable = Mock )
47- @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.asyncio.sleep" )
44+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.threading.Thread" )
45+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.time.sleep" )
4846@patch ("src.sum_impact_assessment.services.full_impact_refresh_service.JobRepository" )
4947@patch ("src.sum_impact_assessment.services.full_impact_refresh_service.get_db_session" )
50- async def test_dispatch_full_refresh_dispatches_all_children (
48+ def test_dispatch_full_refresh_sync_dispatches_all_children (
5149 mock_get_db_session ,
5250 mock_job_repository_class ,
5351 mock_sleep ,
54- mock_to_thread ,
55- mock_create_task ,
52+ mock_thread_class ,
5653):
5754 """Dispatching the full refresh should create all child job runs and mark the parent successful."""
5855 started_at = datetime (2026 , 5 , 12 , 10 , 0 , 0 )
@@ -66,14 +63,13 @@ async def test_dispatch_full_refresh_dispatches_all_children(
6663 repo_instance = Mock ()
6764 repo_instance .create_job_run .side_effect = child_job_runs
6865 mock_job_repository_class .return_value = repo_instance
69- mock_to_thread .return_value = Mock ()
7066
7167 db_context = Mock ()
7268 db_context .__enter__ = Mock (return_value = Mock ())
7369 db_context .__exit__ = Mock (return_value = None )
7470 mock_get_db_session .return_value = db_context
7571
76- await dispatch_full_refresh ("parent-1" , plan )
72+ dispatch_full_refresh_sync ("parent-1" , plan )
7773
7874 assert repo_instance .create_job_run .call_count == 7
7975 assert repo_instance .update_dispatched_job .call_count == 7
@@ -84,27 +80,69 @@ async def test_dispatch_full_refresh_dispatches_all_children(
8480 )
8581 final_call = repo_instance .update_job_status .call_args_list [- 1 ]
8682 assert final_call .kwargs ["status" ] == JobStatusEnum .SUCCESS
87- assert mock_create_task .call_count == 7
88- assert mock_sleep .await_count == 6
89- first_dispatch = mock_to_thread .call_args_list [0 ]
90- assert first_dispatch .args [2 ] == "child-0"
83+ assert mock_thread_class .call_count == 7
84+ assert mock_thread_class .return_value .start .call_count == 7
85+ assert mock_sleep .call_count == 6 # no trailing sleep after the last job
86+ first_thread_call = mock_thread_class .call_args_list [0 ]
87+ assert first_thread_call .kwargs ["args" ][1 ] == "child-0"
9188 first_update = repo_instance .update_dispatched_job .call_args_list [0 ]
9289 assert first_update .args [2 ]["job_run_id" ] == "child-0"
9390 assert first_update .args [2 ]["scheduled_at" ] == "2026-05-12T10:00:00"
9491
9592
96- @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.asyncio.run" )
97- @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.dispatch_full_refresh" , new_callable = Mock )
98- def test_dispatch_full_refresh_sync_uses_asyncio_run (
99- mock_dispatch_full_refresh ,
100- mock_asyncio_run ,
93+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.threading.Thread" )
94+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.time.sleep" )
95+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.JobRepository" )
96+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.get_db_session" )
97+ def test_dispatch_full_refresh_sync_all_seven_children_are_started (
98+ mock_get_db_session ,
99+ mock_job_repository_class ,
100+ mock_sleep ,
101+ mock_thread_class ,
101102):
102- """The sync wrapper should delegate the coroutine through asyncio.run."""
103- plan = [{"job_name" : "kpi_measures_analysis" }]
104- mock_coroutine = object ()
105- mock_dispatch_full_refresh .return_value = mock_coroutine
103+ """Regression: all 7 configured jobs must be passed to a started Thread — including the last one.
106104
107- dispatch_full_refresh_sync ("parent-1" , plan )
105+ Previously the last child (mcda_analysis_qualitative_nsm_providers) was never executed because
106+ asyncio.create_task was used without awaiting, and asyncio.run() closed the loop before the
107+ task had a chance to run in Docker environments with CPU contention.
108+ """
109+ started_at = datetime (2026 , 5 , 12 , 10 , 0 , 0 )
110+ plan = build_dispatch_plan (started_at )
111+
112+ child_job_runs = [
113+ Mock (id = f"child-{ i } " , created_at = datetime (2026 , 5 , 12 , 10 , 0 , i ))
114+ for i in range (len (plan ))
115+ ]
116+
117+ repo_instance = Mock ()
118+ repo_instance .create_job_run .side_effect = child_job_runs
119+ mock_job_repository_class .return_value = repo_instance
120+
121+ db_context = Mock ()
122+ db_context .__enter__ = Mock (return_value = Mock ())
123+ db_context .__exit__ = Mock (return_value = None )
124+ mock_get_db_session .return_value = db_context
125+
126+ dispatch_full_refresh_sync ("parent-reg" , plan )
127+
128+ thread_child_ids = [
129+ call .kwargs ["args" ][1 ]
130+ for call in mock_thread_class .call_args_list
131+ ]
132+ expected_child_ids = [f"child-{ i } " for i in range (7 )]
133+ assert thread_child_ids == expected_child_ids , (
134+ f"Expected all 7 children to be started, got: { thread_child_ids } "
135+ )
136+
137+
138+ @pytest .mark .anyio
139+ @patch ("src.sum_impact_assessment.services.full_impact_refresh_service.dispatch_full_refresh_sync" )
140+ async def test_dispatch_full_refresh_delegates_to_sync (
141+ mock_dispatch_full_refresh_sync ,
142+ ):
143+ """The async wrapper should delegate to dispatch_full_refresh_sync."""
144+ plan = build_dispatch_plan (datetime (2026 , 5 , 12 , 10 , 0 , 0 ))
145+
146+ await dispatch_full_refresh ("parent-1" , plan )
108147
109- mock_dispatch_full_refresh .assert_called_once_with ("parent-1" , plan )
110- mock_asyncio_run .assert_called_once_with (mock_coroutine )
148+ mock_dispatch_full_refresh_sync .assert_called_once_with ("parent-1" , plan )
0 commit comments