1616from temporalio import activity , common , nexus , workflow
1717from temporalio .client import (
1818 Client ,
19+ WorkflowExecutionStatus ,
1920 WorkflowFailureError ,
2021 WorkflowHandle ,
21- WorkflowQueryFailedError ,
2222)
2323from temporalio .contrib .langsmith import LangSmithPlugin
2424from temporalio .exceptions import ApplicationError
25- from temporalio .service import RPCError
2625from temporalio .testing import WorkflowEnvironment
2726from tests .contrib .langsmith .conftest import (
2827 InMemoryRunCollector ,
@@ -312,22 +311,28 @@ def _make_temporal_client(
312311 return Client (** config )
313312
314313
315- @traceable (name = "poll_query" )
316- async def _poll_query (
317- handle : WorkflowHandle [Any , Any ],
318- query : Callable [..., Any ],
319- * ,
320- expected : Any = True ,
321- ) -> bool :
322- """Poll a workflow query until it returns the expected value."""
314+ async def _wait_for_workflow_idle (handle : WorkflowHandle [Any , Any ]) -> None :
315+ """Wait until the running workflow has no pending task, activity, child or Nexus op."""
323316 while True :
324- try :
325- result = await handle .query (query )
326- if result == expected :
327- return True
328- except (WorkflowQueryFailedError , RPCError ):
329- pass # Query not yet available (workflow hasn't started)
330- await asyncio .sleep (1 )
317+ desc = await handle .describe ()
318+ assert desc .status == WorkflowExecutionStatus .RUNNING , desc .status
319+ raw = desc .raw_description
320+ if not (
321+ raw .HasField ("pending_workflow_task" )
322+ or raw .pending_activities
323+ or raw .pending_children
324+ or raw .pending_nexus_operations
325+ ):
326+ return
327+ await asyncio .sleep (0.2 )
328+
329+
330+ @traceable (name = "query_pipeline" )
331+ async def _query_pipeline (
332+ handle : WorkflowHandle [Any , Any ], query : Callable [..., Any ]
333+ ) -> Any :
334+ """Run a single workflow query under a @traceable root."""
335+ return await handle .query (query )
331336
332337
333338# ---------------------------------------------------------------------------
@@ -563,7 +568,7 @@ async def test_comprehensive_with_temporal_runs(
563568 """Full trace hierarchy with worker restart mid-workflow.
564569
565570 user_pipeline only wraps start_workflow (completing before the worker
566- starts), so poll /signal/query traces are naturally separate root traces.
571+ starts), so query /signal/update traces are naturally separate root traces.
567572 """
568573 if env .supports_time_skipping :
569574 pytest .skip ("Time-skipping server doesn't persist headers." )
@@ -575,6 +580,8 @@ async def test_comprehensive_with_temporal_runs(
575580 temporal_client_1 = _make_temporal_client (
576581 client , mock_ls , add_temporal_runs = True
577582 )
583+ # Raw-client handle (no LangSmith interceptor) for untraced readiness checks
584+ raw_handle = client .get_workflow_handle (workflow_id )
578585
579586 @traceable (name = "user_pipeline" )
580587 async def user_pipeline () -> WorkflowHandle [Any , Any ]:
@@ -588,7 +595,7 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
588595 # Start workflow — no worker yet, just a server RPC
589596 handle = await user_pipeline ()
590597
591- # Phase 1: worker picks up workflow, poll until signal wait
598+ # Phase 1: worker runs the workflow up to the signal wait
592599 async with new_worker (
593600 temporal_client_1 ,
594601 ComprehensiveWorkflow ,
@@ -602,14 +609,14 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
602609 make_nexus_endpoint_name (worker .task_queue ),
603610 worker .task_queue ,
604611 )
605- assert await _poll_query (
606- handle ,
607- ComprehensiveWorkflow .is_waiting_for_signal ,
608- expected = True ,
612+ await _wait_for_workflow_idle (raw_handle )
613+ assert await _query_pipeline (
614+ handle , ComprehensiveWorkflow .is_waiting_for_signal
609615 ), "Workflow never reached signal wait point"
610- # Raw-client query (no LangSmith interceptor) — root-level trace
611- raw_handle = client .get_workflow_handle (workflow_id )
612- await raw_handle .query (ComprehensiveWorkflow .is_waiting_for_signal )
616+ # Raw-client query — root-level trace
617+ assert await raw_handle .query (
618+ ComprehensiveWorkflow .is_waiting_for_signal
619+ )
613620
614621 # Phase 2: fresh worker, signal to resume, complete
615622 temporal_client_2 = _make_temporal_client (
@@ -627,6 +634,7 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
627634 handle_2 = temporal_client_2 .get_workflow_handle (workflow_id )
628635 await handle_2 .query (ComprehensiveWorkflow .my_query )
629636 await handle_2 .signal (ComprehensiveWorkflow .my_signal , "hello" )
637+ await _wait_for_workflow_idle (raw_handle )
630638 await handle_2 .execute_update (
631639 ComprehensiveWorkflow .my_unvalidated_update , "test"
632640 )
@@ -704,18 +712,15 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
704712 ]
705713 assert_trace_hierarchy (workflow_trace_trees , expected_workflow )
706714
707- # poll_query trace (separate root, variable number of iterations)
708- poll_trace_trees = find_trace_trees (trace_trees , "poll_query" )
709- assert len (poll_trace_trees ) == 1
710- poll = poll_trace_trees [0 ]
711- assert poll .name == "poll_query"
712- poll_children = poll .children
713- for poll_child in poll_children :
714- assert poll_child .name == "QueryWorkflow:is_waiting_for_signal"
715- assert [child .name for child in poll_child .children ] == [
716- "HandleQuery:is_waiting_for_signal"
717- ]
718- assert not poll_child .children [0 ].children
715+ # query_pipeline trace: the worker-side handler nests under the client query
716+ assert_trace_hierarchy (
717+ find_trace_trees (trace_trees , "query_pipeline" ),
718+ [
719+ "query_pipeline" ,
720+ " QueryWorkflow:is_waiting_for_signal" ,
721+ " HandleQuery:is_waiting_for_signal" ,
722+ ],
723+ )
719724
720725 # Raw-client query — no parent context, appears as root
721726 raw_query_trace_trees = [
@@ -777,7 +782,7 @@ async def test_comprehensive_without_temporal_runs(
777782 """Same workflow with add_temporal_runs=False and worker restart.
778783
779784 Only @traceable runs appear. Context propagation via headers still works.
780- user_pipeline only wraps start_workflow, so poll traces are separate roots .
785+ user_pipeline only wraps start_workflow, so the query trace is a separate root .
781786 """
782787 if env .supports_time_skipping :
783788 pytest .skip ("Time-skipping server doesn't persist headers." )
@@ -789,6 +794,8 @@ async def test_comprehensive_without_temporal_runs(
789794 temporal_client_1 = _make_temporal_client (
790795 client , mock_ls , add_temporal_runs = False
791796 )
797+ # Raw-client handle (no LangSmith interceptor) for untraced readiness checks
798+ raw_handle = client .get_workflow_handle (workflow_id )
792799
793800 @traceable (name = "user_pipeline" )
794801 async def user_pipeline () -> WorkflowHandle [Any , Any ]:
@@ -801,7 +808,7 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
801808 with tracing_context (client = mock_ls , enabled = True ):
802809 handle = await user_pipeline ()
803810
804- # Phase 1: worker picks up workflow, poll until signal wait
811+ # Phase 1: worker runs the workflow up to the signal wait
805812 async with new_worker (
806813 temporal_client_1 ,
807814 ComprehensiveWorkflow ,
@@ -815,13 +822,13 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
815822 make_nexus_endpoint_name (worker .task_queue ),
816823 worker .task_queue ,
817824 )
825+ await _wait_for_workflow_idle (raw_handle )
818826 # Raw-client query — no interceptor, produces nothing
819- raw_handle = client .get_workflow_handle (workflow_id )
820- await raw_handle .query (ComprehensiveWorkflow .is_waiting_for_signal )
821- assert await _poll_query (
822- handle ,
823- ComprehensiveWorkflow .is_waiting_for_signal ,
824- expected = True ,
827+ assert await raw_handle .query (
828+ ComprehensiveWorkflow .is_waiting_for_signal
829+ )
830+ assert await _query_pipeline (
831+ handle , ComprehensiveWorkflow .is_waiting_for_signal
825832 ), "Workflow never reached signal wait point"
826833
827834 # Phase 2: fresh worker, signal to resume, complete
@@ -839,6 +846,7 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
839846 ):
840847 handle_2 = temporal_client_2 .get_workflow_handle (workflow_id )
841848 await handle_2 .signal (ComprehensiveWorkflow .my_signal , "hello" )
849+ await _wait_for_workflow_idle (raw_handle )
842850 await handle_2 .execute_update (
843851 ComprehensiveWorkflow .my_unvalidated_update , "test"
844852 )
@@ -883,10 +891,10 @@ async def user_pipeline() -> WorkflowHandle[Any, Any]:
883891 ]
884892 assert_trace_hierarchy (workflow_trace_trees , expected_workflow )
885893
886- # Poll query — separate root, just the @traceable wrapper, no Temporal children
887- poll_trace_trees = find_trace_trees ( trace_trees , "poll_query" )
888- assert len ( poll_trace_trees ) == 1
889- assert_trace_hierarchy ( poll_trace_trees , [ "poll_query" ] )
894+ # Query — separate root, just the @traceable wrapper, no Temporal children
895+ assert_trace_hierarchy (
896+ find_trace_trees ( trace_trees , "query_pipeline" ), [ "query_pipeline" ]
897+ )
890898
891899
892900# ---------------------------------------------------------------------------
@@ -1252,13 +1260,7 @@ async def test_temporal_prefixed_query_not_traced(
12521260 task_queue = worker .task_queue ,
12531261 )
12541262
1255- # Wait for workflow to start by polling the user query
1256- assert await _poll_query (
1257- handle ,
1258- QueryFilteringWorkflow .my_query ,
1259- expected = "query-result" ,
1260- ), "Workflow never started"
1261-
1263+ await _wait_for_workflow_idle (handle )
12621264 collector .clear ()
12631265
12641266 # Built-in queries — should NOT be traced
0 commit comments