2323
2424
2525class TestOutputScope (unittest .TestCase ):
26- """Unit tests for OutputScope and its methods ."""
26+ """Unit tests for OutputScope."""
2727
2828 @classmethod
2929 def setUpClass (cls ):
3030 """Set up test environment once for all tests."""
31- # Configure Microsoft Agent 365 for testing
3231 os .environ ["ENABLE_A365_OBSERVABILITY" ] = "true"
3332
3433 configure (
3534 service_name = "test-output-scope-service" ,
3635 service_namespace = "test-namespace" ,
3736 )
38- # Create test data
37+
3938 cls .tenant_details = TenantDetails (tenant_id = "12345678-1234-5678-1234-567812345678" )
4039 cls .agent_details = AgentDetails (
4140 agent_id = "test-agent-123" ,
@@ -46,18 +45,16 @@ def setUpClass(cls):
4645 def setUp (self ):
4746 super ().setUp ()
4847
49- # Reset TelemetryManager state to ensure fresh configuration
48+ # Reset TelemetryManager state
5049 _telemetry_manager ._tracer_provider = None
5150 _telemetry_manager ._span_processors = {}
5251 OpenTelemetryScope ._tracer = None
5352
54- # Reconfigure to get a fresh TracerProvider
5553 configure (
5654 service_name = "test-output-scope-service" ,
5755 service_namespace = "test-namespace" ,
5856 )
5957
60- # Set up tracer to capture spans
6158 self .span_exporter = InMemorySpanExporter ()
6259 tracer_provider = get_tracer_provider ()
6360 tracer_provider .add_span_processor (SimpleSpanProcessor (self .span_exporter ))
@@ -66,219 +63,86 @@ def tearDown(self):
6663 super ().tearDown ()
6764 self .span_exporter .clear ()
6865
69- def test_output_scope_creation (self ):
70- """Test that OutputScope can be created successfully."""
71- response = Response (messages = ["Hello, how can I help you?" ])
72-
73- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
74-
75- self .assertIsNotNone (scope )
76- scope .dispose ()
77-
78- def test_record_output_messages_method_exists (self ):
79- """Test that record_output_messages method exists on OutputScope."""
80- response = Response (messages = ["Initial message" ])
81- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
82-
83- if scope is not None :
84- # Test that the method exists
85- self .assertTrue (hasattr (scope , "record_output_messages" ))
86- self .assertTrue (callable (scope .record_output_messages ))
87- scope .dispose ()
88-
89- def test_output_messages_set_on_span (self ):
90- """Test that output messages are set on span attributes."""
91- response = Response (messages = ["This is the agent response" ])
92-
93- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
94-
95- if scope is not None :
96- scope .dispose ()
97-
98- finished_spans = self .span_exporter .get_finished_spans ()
99- self .assertTrue (finished_spans , "Expected at least one span to be created" )
100-
101- span = finished_spans [- 1 ]
102- span_attributes = getattr (span , "attributes" , {}) or {}
103-
104- self .assertIn (
105- GEN_AI_OUTPUT_MESSAGES_KEY ,
106- span_attributes ,
107- "Expected output messages key to be set on span" ,
108- )
109-
110- # Verify the message content is in the serialized output
111- output_value = span_attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
112- self .assertIn ("This is the agent response" , output_value )
113-
114- def test_multiple_output_messages (self ):
115- """Test that multiple output messages are properly recorded."""
116- response = Response (messages = ["First response" , "Second response" , "Third response" ])
117-
118- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
119-
120- if scope is not None :
121- scope .dispose ()
122-
123- finished_spans = self .span_exporter .get_finished_spans ()
124- self .assertTrue (finished_spans , "Expected at least one span to be created" )
125-
126- span = finished_spans [- 1 ]
127- span_attributes = getattr (span , "attributes" , {}) or {}
128-
129- self .assertIn (
130- GEN_AI_OUTPUT_MESSAGES_KEY ,
131- span_attributes ,
132- "Expected output messages key to be set on span" ,
133- )
134-
135- output_value = span_attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
136- self .assertIn ("First response" , output_value )
137- self .assertIn ("Second response" , output_value )
138- self .assertIn ("Third response" , output_value )
139-
140- def test_record_output_messages_updates_span (self ):
141- """Test that record_output_messages appends messages to the span."""
142- response = Response (messages = ["Initial message" ])
143-
144- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
145-
146- if scope is not None :
147- # Record additional messages (should append, not replace)
148- scope .record_output_messages (["Appended message 1" , "Appended message 2" ])
149- scope .dispose ()
150-
66+ def _get_last_span (self ):
67+ """Helper to get the last finished span and its attributes."""
15168 finished_spans = self .span_exporter .get_finished_spans ()
15269 self .assertTrue (finished_spans , "Expected at least one span to be created" )
153-
15470 span = finished_spans [- 1 ]
155- span_attributes = getattr (span , "attributes" , {}) or {}
156-
157- self .assertIn (
158- GEN_AI_OUTPUT_MESSAGES_KEY ,
159- span_attributes ,
160- "Expected output messages key to be set on span" ,
161- )
162-
163- # The span should have all messages (initial + appended)
164- output_value = span_attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
165- self .assertIn ("Initial message" , output_value )
166- self .assertIn ("Appended message 1" , output_value )
167- self .assertIn ("Appended message 2" , output_value )
168-
169- def test_record_output_messages_multiple_appends (self ):
170- """Test that multiple calls to record_output_messages accumulate messages."""
171- response = Response (messages = ["First message" ])
71+ attributes = getattr (span , "attributes" , {}) or {}
72+ return span , attributes
17273
173- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
74+ def test_output_scope_creates_span_with_messages (self ):
75+ """Test OutputScope creates span with output messages attribute."""
76+ response = Response (messages = ["First message" , "Second message" ])
17477
175- if scope is not None :
176- # First append
177- scope .record_output_messages (["Second message" ])
178- # Second append
179- scope .record_output_messages (["Third message" , "Fourth message" ])
180- scope .dispose ()
78+ with OutputScope .start (self .agent_details , self .tenant_details , response ):
79+ pass
18180
182- finished_spans = self .span_exporter .get_finished_spans ()
183- self .assertTrue (finished_spans , "Expected at least one span to be created" )
81+ span , attributes = self ._get_last_span ()
18482
185- span = finished_spans [- 1 ]
186- span_attributes = getattr (span , "attributes" , {}) or {}
83+ # Verify span name contains operation name and agent id
84+ self .assertIn ("output_messages" , span .name )
85+ self .assertIn (self .agent_details .agent_id , span .name )
18786
188- output_value = span_attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
189- # All four messages should be present
87+ # Verify output messages are set
88+ self .assertIn (GEN_AI_OUTPUT_MESSAGES_KEY , attributes )
89+ output_value = attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
19090 self .assertIn ("First message" , output_value )
19191 self .assertIn ("Second message" , output_value )
192- self .assertIn ("Third message" , output_value )
193- self .assertIn ("Fourth message" , output_value )
19492
195- def test_output_scope_context_manager (self ):
196- """Test that OutputScope works as a context manager ."""
197- response = Response (messages = ["Context manager test " ])
93+ def test_record_output_messages_appends (self ):
94+ """Test record_output_messages appends to accumulated messages ."""
95+ response = Response (messages = ["Initial " ])
19896
19997 with OutputScope .start (self .agent_details , self .tenant_details , response ) as scope :
200- self .assertIsNotNone (scope )
201-
202- finished_spans = self .span_exporter .get_finished_spans ()
203- self .assertTrue (finished_spans , "Expected at least one span to be created" )
204-
205- def test_output_scope_span_name (self ):
206- """Test that OutputScope creates spans with correct operation name."""
207- response = Response (messages = ["Test message" ])
98+ scope .record_output_messages (["Appended 1" ])
99+ scope .record_output_messages (["Appended 2" , "Appended 3" ])
208100
209- scope = OutputScope .start (self .agent_details , self .tenant_details , response )
210-
211- if scope is not None :
212- scope .dispose ()
213-
214- finished_spans = self .span_exporter .get_finished_spans ()
215- self .assertTrue (finished_spans , "Expected at least one span to be created" )
101+ _ , attributes = self ._get_last_span ()
216102
217- span = finished_spans [- 1 ]
218- # The activity name should contain "output_messages" and the agent id
219- self .assertIn ("output_messages" , span .name )
220- self .assertIn (self .agent_details .agent_id , span .name )
103+ output_value = attributes [GEN_AI_OUTPUT_MESSAGES_KEY ]
104+ # All messages should be present (initial + all appended)
105+ self .assertIn ("Initial" , output_value )
106+ self .assertIn ("Appended 1" , output_value )
107+ self .assertIn ("Appended 2" , output_value )
108+ self .assertIn ("Appended 3" , output_value )
221109
222110 def test_output_scope_with_parent_id (self ):
223- """Test that OutputScope uses parent_id to link span to parent."""
224- response = Response (messages = ["Test message with parent" ])
225- # W3C Trace Context format: "00-{trace_id}-{span_id}-{trace_flags}"
226- # trace_id: 32 hex chars, span_id: 16 hex chars
111+ """Test OutputScope uses parent_id to link span to parent context."""
112+ response = Response (messages = ["Test" ])
227113 parent_trace_id = "1234567890abcdef1234567890abcdef"
228114 parent_span_id = "abcdefabcdef1234"
229115 parent_id = f"00-{ parent_trace_id } -{ parent_span_id } -01"
230116
231- scope = OutputScope .start (
117+ with OutputScope .start (
232118 self .agent_details , self .tenant_details , response , parent_id = parent_id
233- )
119+ ):
120+ pass
234121
235- if scope is not None :
236- scope .dispose ()
237-
238- finished_spans = self .span_exporter .get_finished_spans ()
239- self .assertTrue (finished_spans , "Expected at least one span to be created" )
240-
241- span = finished_spans [- 1 ]
122+ span , _ = self ._get_last_span ()
242123
243- # Verify the span has the correct parent trace context
244- # The span's trace_id should match the parent's trace_id
124+ # Verify span inherits parent's trace_id
245125 span_trace_id = f"{ span .context .trace_id :032x} "
246- self .assertEqual (
247- span_trace_id ,
248- parent_trace_id ,
249- "Expected span's trace_id to match parent's trace_id" ,
250- )
126+ self .assertEqual (span_trace_id , parent_trace_id )
251127
252- # The span's parent_span_id should match the parent's span_id
253- span_parent_id = None
254- if span .parent and hasattr (span .parent , "span_id" ):
255- span_parent_id = f"{ span .parent .span_id :016x} "
256- self .assertEqual (
257- span_parent_id ,
258- parent_span_id ,
259- "Expected span's parent_span_id to match parent's span_id" ,
260- )
128+ # Verify span's parent_span_id matches
129+ self .assertIsNotNone (span .parent , "Expected span to have a parent" )
130+ self .assertTrue (hasattr (span .parent , "span_id" ), "Expected parent to have span_id" )
131+ span_parent_id = f"{ span .parent .span_id :016x} "
132+ self .assertEqual (span_parent_id , parent_span_id )
261133
262- def test_output_scope_without_parent_id (self ):
263- """Test that OutputScope creates a span without forced parent when not provided ."""
264- response = Response (messages = ["Test message without parent " ])
134+ def test_output_scope_dispose (self ):
135+ """Test OutputScope dispose method ends the span ."""
136+ response = Response (messages = ["Test" ])
265137
266138 scope = OutputScope .start (self .agent_details , self .tenant_details , response )
139+ self .assertIsNotNone (scope )
140+ scope .dispose ()
267141
268- if scope is not None :
269- scope .dispose ()
270-
142+ # Verify span was created and ended
271143 finished_spans = self .span_exporter .get_finished_spans ()
272- self .assertTrue (finished_spans , "Expected at least one span to be created" )
273-
274- span = finished_spans [- 1 ]
275-
276- # When no parent_id is provided, the span should either have no parent
277- # or inherit from the current context (which in tests is typically empty)
278- # We just verify the span was created successfully
279- self .assertIsNotNone (span .context .span_id )
144+ self .assertEqual (len (finished_spans ), 1 )
280145
281146
282147if __name__ == "__main__" :
283- # Run pytest only on the current file
284148 sys .exit (pytest .main ([str (Path (__file__ ))] + sys .argv [1 :]))
0 commit comments