@@ -684,6 +684,152 @@ async def test_update_payload_conversion(
684684 )
685685
686686
687+ # External workflow test
688+
689+
690+ @workflow .defn
691+ class ExternalWorkflowTarget :
692+ def __init__ (self ) -> None :
693+ self .signal_received = None
694+
695+ @workflow .run
696+ async def run (self ) -> TraceData :
697+ try :
698+ # Wait for signal
699+ await workflow .wait_condition (lambda : self .signal_received is not None )
700+ return self .signal_received or TraceData ()
701+ except asyncio .CancelledError :
702+ # Return empty data on cancellation
703+ return TraceData ()
704+
705+ @workflow .signal
706+ async def external_signal (self , data : TraceData ) -> None :
707+ self .signal_received = data
708+
709+
710+ @workflow .defn
711+ class ExternalWorkflowSignaler :
712+ @workflow .run
713+ async def run (self , target_id : str , data : TraceData ) -> TraceData :
714+ # Signal external workflow
715+ handle = workflow .get_external_workflow_handle (target_id )
716+ await handle .signal (ExternalWorkflowTarget .external_signal , data )
717+ return data
718+
719+
720+ @workflow .defn
721+ class ExternalWorkflowCanceller :
722+ @workflow .run
723+ async def run (self , target_id : str ) -> TraceData :
724+ # Cancel external workflow
725+ handle = workflow .get_external_workflow_handle (target_id )
726+ await handle .cancel ()
727+ return TraceData ()
728+
729+
730+ @pytest .mark .timeout (10 )
731+ async def test_external_workflow_signal_and_cancel_payload_conversion (
732+ client : Client ,
733+ ):
734+ target_workflow_id = str (uuid .uuid4 ())
735+ signaler_workflow_id = str (uuid .uuid4 ())
736+ canceller_workflow_id = str (uuid .uuid4 ())
737+ task_queue = str (uuid .uuid4 ())
738+
739+ data_converter = dataclasses .replace (
740+ DataConverter .default ,
741+ payload_converter_class = SerializationContextTestPayloadConverter ,
742+ )
743+
744+ config = client .config ()
745+ config ["data_converter" ] = data_converter
746+ custom_client = Client (** config )
747+
748+ async with Worker (
749+ custom_client ,
750+ task_queue = task_queue ,
751+ workflows = [
752+ ExternalWorkflowTarget ,
753+ ExternalWorkflowSignaler ,
754+ ExternalWorkflowCanceller ,
755+ ],
756+ activities = [],
757+ workflow_runner = UnsandboxedWorkflowRunner (), # so that we can use isinstance
758+ ):
759+ # Test external signal
760+ target_handle = await custom_client .start_workflow (
761+ ExternalWorkflowTarget .run ,
762+ id = target_workflow_id ,
763+ task_queue = task_queue ,
764+ )
765+
766+ signaler_handle = await custom_client .start_workflow (
767+ ExternalWorkflowSignaler .run ,
768+ args = [target_workflow_id , TraceData ()],
769+ id = signaler_workflow_id ,
770+ task_queue = task_queue ,
771+ )
772+
773+ # Wait for both to complete
774+ signaler_result = await signaler_handle .result ()
775+ target_result = await target_handle .result ()
776+
777+ # Verify signal trace
778+ signaler_context = dataclasses .asdict (
779+ WorkflowSerializationContext (
780+ namespace = "default" ,
781+ workflow_id = signaler_workflow_id ,
782+ )
783+ )
784+ target_context = dataclasses .asdict (
785+ WorkflowSerializationContext (
786+ namespace = "default" ,
787+ workflow_id = target_workflow_id ,
788+ )
789+ )
790+
791+ # This test verifies that external signals SHOULD use the target workflow's context
792+ # This is the DESIRED behavior to match .NET
793+ assert_trace (
794+ signaler_result .items ,
795+ [
796+ TraceItem (
797+ context_type = "workflow" ,
798+ in_workflow = False ,
799+ method = "to_payload" ,
800+ context = signaler_context , # Outbound signaler workflow input
801+ ),
802+ TraceItem (
803+ context_type = "workflow" ,
804+ in_workflow = False ,
805+ method = "from_payload" ,
806+ context = signaler_context , # Inbound signaler workflow input
807+ ),
808+ TraceItem (
809+ context_type = "workflow" ,
810+ in_workflow = True ,
811+ method = "to_payload" ,
812+ context = target_context , # Should use target workflow's context for external signal
813+ ),
814+ TraceItem (
815+ context_type = "workflow" ,
816+ in_workflow = True ,
817+ method = "to_payload" ,
818+ context = signaler_context , # Outbound signaler workflow result
819+ ),
820+ TraceItem (
821+ context_type = "workflow" ,
822+ in_workflow = False ,
823+ method = "from_payload" ,
824+ context = signaler_context , # Inbound signaler workflow result
825+ ),
826+ ],
827+ )
828+
829+ # Note: External cancel doesn't send payloads, so we don't test it here
830+ # The cancel context would only be used for failure deserialization
831+
832+
687833# Utilities
688834
689835
0 commit comments