33from datetime import datetime , timezone
44from unittest .mock import AsyncMock , patch
55
6+ import pytest
7+ from temporalio .exceptions import ActivityError
8+
69import agentex .lib .adk ._modules .tracing as _tracing_mod
710from agentex .types .span import Span
811from agentex .lib .adk ._modules .tracing import TracingModule
@@ -26,6 +29,18 @@ def _make_module() -> tuple[AsyncMock, TracingModule]:
2629 return mock_service , module
2730
2831
32+ def _make_activity_error () -> ActivityError :
33+ return ActivityError (
34+ "activity timed out" ,
35+ scheduled_event_id = 1 ,
36+ started_event_id = 2 ,
37+ identity = "worker-1" ,
38+ activity_type = "start-span" ,
39+ activity_id = "activity-1" ,
40+ retry_state = None ,
41+ )
42+
43+
2944class TestStartSpan :
3045 async def test_start_span_with_task_id (self ):
3146 mock_service , module = _make_module ()
@@ -87,6 +102,102 @@ async def test_end_span_preserves_task_id(self):
87102 mock_service .end_span .assert_called_once_with (trace_id = "trace-123" , span = span )
88103
89104
105+ class TestTracingModuleTemporalPath :
106+ async def test_start_span_in_workflow_returns_none_when_activity_fails (self ):
107+ mock_service , module = _make_module ()
108+
109+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
110+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers , \
111+ patch .object (_tracing_mod .workflow , "logger" ) as mock_logger :
112+ mock_helpers .execute_activity = AsyncMock (side_effect = _make_activity_error ())
113+ result = await module .start_span (trace_id = "trace-123" , name = "test-span" )
114+
115+ assert result is None
116+ mock_logger .warning .assert_called_once ()
117+ mock_helpers .execute_activity .assert_called_once ()
118+ mock_service .start_span .assert_not_called ()
119+
120+ async def test_end_span_in_workflow_returns_span_when_activity_fails (self ):
121+ mock_service , module = _make_module ()
122+ span = _make_span ()
123+
124+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
125+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers , \
126+ patch .object (_tracing_mod .workflow , "logger" ) as mock_logger :
127+ mock_helpers .execute_activity = AsyncMock (side_effect = _make_activity_error ())
128+ result = await module .end_span (trace_id = "trace-123" , span = span )
129+
130+ assert result == span
131+ mock_logger .warning .assert_called_once ()
132+ mock_helpers .execute_activity .assert_called_once ()
133+ mock_service .end_span .assert_not_called ()
134+
135+ async def test_context_manager_skips_end_when_temporal_start_fails (self ):
136+ mock_service , module = _make_module ()
137+
138+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
139+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers , \
140+ patch .object (_tracing_mod .workflow , "logger" ):
141+ mock_helpers .execute_activity = AsyncMock (side_effect = _make_activity_error ())
142+ async with module .span (trace_id = "trace-123" , name = "test-span" ) as span :
143+ assert span is None
144+
145+ mock_helpers .execute_activity .assert_called_once ()
146+ mock_service .start_span .assert_not_called ()
147+ mock_service .end_span .assert_not_called ()
148+
149+ async def test_start_span_in_workflow_propagates_unexpected_errors (self ):
150+ mock_service , module = _make_module ()
151+
152+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
153+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers :
154+ mock_helpers .execute_activity = AsyncMock (side_effect = RuntimeError ("bad response shape" ))
155+ try :
156+ await module .start_span (trace_id = "trace-123" , name = "test-span" )
157+ except RuntimeError as exc :
158+ assert str (exc ) == "bad response shape"
159+ else :
160+ raise AssertionError ("Expected unexpected errors to propagate" )
161+
162+ mock_helpers .execute_activity .assert_called_once ()
163+ mock_service .start_span .assert_not_called ()
164+
165+ async def test_start_span_in_workflow_propagates_cancellation (self ):
166+ mock_service , module = _make_module ()
167+ activity_error = _make_activity_error ()
168+
169+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
170+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers , \
171+ patch .object (_tracing_mod , "is_cancelled_exception" , return_value = True ), \
172+ patch .object (_tracing_mod .workflow , "logger" ) as mock_logger :
173+ mock_helpers .execute_activity = AsyncMock (side_effect = activity_error )
174+
175+ with pytest .raises (ActivityError ):
176+ await module .start_span (trace_id = "trace-123" , name = "test-span" )
177+
178+ mock_logger .warning .assert_not_called ()
179+ mock_helpers .execute_activity .assert_called_once ()
180+ mock_service .start_span .assert_not_called ()
181+
182+ async def test_end_span_in_workflow_propagates_cancellation (self ):
183+ mock_service , module = _make_module ()
184+ span = _make_span ()
185+ activity_error = _make_activity_error ()
186+
187+ with patch .object (_tracing_mod , "in_temporal_workflow" , return_value = True ), \
188+ patch .object (_tracing_mod , "ActivityHelpers" ) as mock_helpers , \
189+ patch .object (_tracing_mod , "is_cancelled_exception" , return_value = True ), \
190+ patch .object (_tracing_mod .workflow , "logger" ) as mock_logger :
191+ mock_helpers .execute_activity = AsyncMock (side_effect = activity_error )
192+
193+ with pytest .raises (ActivityError ):
194+ await module .end_span (trace_id = "trace-123" , span = span )
195+
196+ mock_logger .warning .assert_not_called ()
197+ mock_helpers .execute_activity .assert_called_once ()
198+ mock_service .end_span .assert_not_called ()
199+
200+
90201class TestSpanContextManager :
91202 async def test_span_context_manager_forwards_task_id (self ):
92203 mock_service , module = _make_module ()
0 commit comments