@@ -1149,16 +1149,81 @@ async def started() -> bool:
11491149 # cancel has already been observed.
11501150 assert await handle .query (CancelReasonWorkflow .reason_inner ) is None
11511151
1152- await handle .cancel ()
1152+ await handle .cancel (reason = "user-supplied reason" )
11531153 with pytest .raises (WorkflowFailureError ) as err :
11541154 await handle .result ()
11551155 assert isinstance (err .value .cause , CancelledError )
11561156
1157- # After external cancel, reason is a string (empty since the Python
1158- # client does not send one) — non-None is the load-bearing distinction.
11591157 outer = await handle .query (CancelReasonWorkflow .reason_outer )
1160- assert outer is not None
1161- assert isinstance (outer , str )
1158+ assert outer == "user-supplied reason"
1159+
1160+
1161+ @workflow .defn
1162+ class CancelReasonReporter :
1163+ """Workflow that swallows a cancel and returns the observed reason."""
1164+
1165+ @workflow .run
1166+ async def run (self ) -> str :
1167+ try :
1168+ await asyncio .sleep (1000 )
1169+ except asyncio .CancelledError :
1170+ return workflow .cancellation_reason () or ""
1171+ raise RuntimeError ("unreachable" )
1172+
1173+
1174+ @workflow .defn
1175+ class ChildCancelReasonWorkflow :
1176+ @workflow .run
1177+ async def run (self , msg : str ) -> str :
1178+ child = await workflow .start_child_workflow (
1179+ CancelReasonReporter .run ,
1180+ id = f"{ workflow .info ().workflow_id } _child" ,
1181+ )
1182+ child .cancel (msg )
1183+ return await child
1184+
1185+
1186+ async def test_workflow_child_cancel_reason (client : Client ):
1187+ async with new_worker (
1188+ client , ChildCancelReasonWorkflow , CancelReasonReporter
1189+ ) as worker :
1190+ result = await client .execute_workflow (
1191+ ChildCancelReasonWorkflow .run ,
1192+ "from-parent" ,
1193+ id = f"workflow-{ uuid .uuid4 ()} " ,
1194+ task_queue = worker .task_queue ,
1195+ )
1196+ assert result == "from-parent"
1197+
1198+
1199+ @workflow .defn
1200+ class ExternalCancelReasonWorkflow :
1201+ @workflow .run
1202+ async def run (self , target_id : str ) -> None :
1203+ await workflow .get_external_workflow_handle (target_id ).cancel (
1204+ reason = "from-external-caller"
1205+ )
1206+
1207+
1208+ async def test_workflow_external_cancel_reason (client : Client ):
1209+ async with new_worker (
1210+ client , ExternalCancelReasonWorkflow , CancelReasonReporter
1211+ ) as worker :
1212+ target_id = f"workflow-{ uuid .uuid4 ()} "
1213+ target = await client .start_workflow (
1214+ CancelReasonReporter .run ,
1215+ id = target_id ,
1216+ task_queue = worker .task_queue ,
1217+ )
1218+ await client .execute_workflow (
1219+ ExternalCancelReasonWorkflow .run ,
1220+ target_id ,
1221+ id = f"workflow-{ uuid .uuid4 ()} " ,
1222+ task_queue = worker .task_queue ,
1223+ )
1224+ # Server wraps the user-supplied reason with metadata about the caller
1225+ # when one workflow cancels another, so check for substring.
1226+ assert "from-external-caller" in await target .result ()
11621227
11631228
11641229@workflow .defn
0 commit comments