77import httpx
88import pytest
99
10+ from durable_workflow .client import WorkflowHandle
1011from durable_workflow .sync import Client , SyncWorkflowHandle
1112
1213
@@ -161,6 +162,101 @@ def test_describe_task_queue(self) -> None:
161162 assert mock .call_args .args [:2 ] == ("GET" , "/api/task-queues/orders%2Fhigh%20priority" )
162163
163164
165+ class TestSyncClientRunVisibility :
166+ def test_list_workflow_runs (self ) -> None :
167+ client = Client ("http://localhost:8080" )
168+ resp = _mock_response (
169+ 200 ,
170+ {
171+ "workflow_id" : "wf-1" ,
172+ "run_count" : 2 ,
173+ "runs" : [
174+ {"workflow_id" : "wf-1" , "run_id" : "r1" , "workflow_type" : "greeter" , "status" : "completed" },
175+ {
176+ "workflow_id" : "wf-1" ,
177+ "run_id" : "r2" ,
178+ "workflow_type" : "greeter" ,
179+ "status" : "running" ,
180+ "is_current_run" : True ,
181+ },
182+ ],
183+ },
184+ )
185+ with patch .object (client ._async ._http , "request" , new_callable = AsyncMock , return_value = resp ) as mock :
186+ result = client .list_workflow_runs ("wf-1" )
187+
188+ assert result .workflow_id == "wf-1"
189+ assert result .run_count == 2
190+ assert [run .run_id for run in result .runs ] == ["r1" , "r2" ]
191+ assert result .runs [1 ].is_current_run is True
192+ assert mock .call_args .args [:2 ] == ("GET" , "/api/workflows/wf-1/runs" )
193+
194+ def test_describe_workflow_run (self ) -> None :
195+ client = Client ("http://localhost:8080" )
196+ resp = _mock_response (
197+ 200 ,
198+ {
199+ "workflow_id" : "wf-1" ,
200+ "run_id" : "r1" ,
201+ "workflow_type" : "greeter" ,
202+ "status" : "completed" ,
203+ "status_bucket" : "terminal" ,
204+ "actions" : {"archive" : {"enabled" : True }},
205+ },
206+ )
207+ with patch .object (client ._async ._http , "request" , new_callable = AsyncMock , return_value = resp ) as mock :
208+ run = client .describe_workflow_run ("wf-1" , "r1" )
209+
210+ assert run .workflow_id == "wf-1"
211+ assert run .run_id == "r1"
212+ assert run .status_bucket == "terminal"
213+ assert run .actions == {"archive" : {"enabled" : True }}
214+ assert mock .call_args .args [:2 ] == ("GET" , "/api/workflows/wf-1/runs/r1" )
215+
216+
217+ class TestSyncClientMaintenance :
218+ def test_repair_workflow (self ) -> None :
219+ client = Client ("http://localhost:8080" )
220+ resp = _mock_response (
221+ 200 ,
222+ {
223+ "workflow_id" : "wf-1" ,
224+ "outcome" : "accepted" ,
225+ "command_status" : "queued" ,
226+ "command_id" : "cmd-1" ,
227+ },
228+ )
229+ with patch .object (client ._async ._http , "request" , new_callable = AsyncMock , return_value = resp ) as mock :
230+ result = client .repair_workflow ("wf-1" )
231+
232+ assert result .workflow_id == "wf-1"
233+ assert result .outcome == "accepted"
234+ assert result .command_status == "queued"
235+ assert result .command_id == "cmd-1"
236+ assert mock .call_args .args [:2 ] == ("POST" , "/api/workflows/wf-1/repair" )
237+
238+ def test_archive_workflow (self ) -> None :
239+ client = Client ("http://localhost:8080" )
240+ resp = _mock_response (
241+ 200 ,
242+ {
243+ "workflow_id" : "wf-1" ,
244+ "outcome" : "accepted" ,
245+ "command_status" : "completed" ,
246+ "command_id" : "cmd-2" ,
247+ },
248+ )
249+ with patch .object (client ._async ._http , "request" , new_callable = AsyncMock , return_value = resp ) as mock :
250+ result = client .archive_workflow ("wf-1" , reason = "retention policy" )
251+
252+ assert result .workflow_id == "wf-1"
253+ assert result .outcome == "accepted"
254+ assert result .command_status == "completed"
255+ assert result .command_id == "cmd-2"
256+ assert mock .call_args .args [:2 ] == ("POST" , "/api/workflows/wf-1/archive" )
257+ assert mock .call_args .kwargs ["json" ] == {"reason" : "retention policy" }
258+
259+
164260class TestSyncClientUpdate :
165261 def test_update (self ) -> None :
166262 client = Client ("http://localhost:8080" )
@@ -187,6 +283,26 @@ def test_handle_update(self) -> None:
187283 assert result ["outcome" ] == "completed"
188284
189285
286+ class TestSyncWorkflowHandleControlPlane :
287+ def test_run_visibility_and_maintenance_delegate_to_async_handle (self ) -> None :
288+ async_handle = WorkflowHandle (AsyncMock (), workflow_id = "wf-1" , run_id = "r1" , workflow_type = "greeter" )
289+ async_handle .get_history = AsyncMock (return_value = {"events" : []}) # type: ignore[method-assign]
290+ async_handle .export_history = AsyncMock (return_value = {"schema" : "durable.workflow.history.v2" }) # type: ignore[method-assign]
291+ async_handle .list_runs = AsyncMock (return_value = []) # type: ignore[method-assign]
292+ async_handle .describe_run = AsyncMock (return_value = {"run_id" : "r1" }) # type: ignore[method-assign]
293+ async_handle .repair = AsyncMock (return_value = {"outcome" : "accepted" }) # type: ignore[method-assign]
294+ async_handle .archive = AsyncMock (return_value = {"outcome" : "completed" }) # type: ignore[method-assign]
295+ handle = SyncWorkflowHandle (async_handle )
296+
297+ assert handle .get_history () == {"events" : []}
298+ assert handle .export_history () == {"schema" : "durable.workflow.history.v2" }
299+ assert handle .list_runs () == []
300+ assert handle .describe_run () == {"run_id" : "r1" }
301+ assert handle .repair () == {"outcome" : "accepted" }
302+ assert handle .archive (reason = "retention" ) == {"outcome" : "completed" }
303+ async_handle .archive .assert_awaited_once_with (reason = "retention" )
304+
305+
190306class TestSyncClientContextManager :
191307 def test_context_manager (self ) -> None :
192308 with Client ("http://localhost:8080" ) as client :
0 commit comments