Skip to content

Commit 9b2473a

Browse files
add tests for new functionality
1 parent 12aaeaf commit 9b2473a

4 files changed

Lines changed: 318 additions & 29 deletions

File tree

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,117 @@
1-
# Copyright (c) Microsoft. All rights reserved.
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
23

4+
import unittest
5+
from unittest.mock import Mock, patch
36

47
from microsoft_agents_a365.observability.core import configure
8+
from microsoft_agents_a365.observability.core.exporters.agent365_exporter_options import (
9+
Agent365ExporterOptions,
10+
)
511
from microsoft_agents_a365.observability.core.trace_processor import SpanProcessor
612

713

8-
def test_basic_functionality():
9-
"""Test basic Microsoft Agent 365 SDK functionality"""
10-
print("Testing Microsoft Agent 365 SDK...")
14+
class TestAgent365Configure(unittest.TestCase):
15+
"""Test suite for Agent365 configuration functionality."""
1116

12-
# Test configure function
13-
try:
14-
configure(
17+
def setUp(self):
18+
"""Set up test fixtures."""
19+
self.mock_token_resolver = Mock()
20+
self.mock_token_resolver.return_value = "test_token_123"
21+
22+
def test_configure_basic_functionality(self):
23+
"""Test configure function with basic parameters and legacy parameters."""
24+
# Test basic configuration without exporter_options
25+
result = configure(
26+
service_name="test-service",
27+
service_namespace="test-namespace",
28+
)
29+
self.assertTrue(result, "configure() should return True")
30+
31+
# Test configuration with legacy parameters
32+
result = configure(
33+
service_name="test-service",
34+
service_namespace="test-namespace",
35+
token_resolver=self.mock_token_resolver,
36+
cluster_category="test",
37+
)
38+
self.assertTrue(result, "configure() should return True with legacy parameters")
39+
40+
@patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled")
41+
def test_configure_with_exporter_options_and_parameter_precedence(self, mock_is_enabled):
42+
"""Test configure function with exporter_options and verify parameter precedence."""
43+
# Enable Agent365 exporter for this test
44+
mock_is_enabled.return_value = True
45+
46+
# Test 1: Basic exporter_options functionality
47+
exporter_options = Agent365ExporterOptions(
48+
cluster_category="dev",
49+
token_resolver=self.mock_token_resolver,
50+
use_s2s_endpoint=True,
51+
max_queue_size=1024,
52+
scheduled_delay_ms=2500,
53+
exporter_timeout_ms=15000,
54+
max_export_batch_size=256,
55+
)
56+
57+
result = configure(
1558
service_name="test-service",
1659
service_namespace="test-namespace",
60+
exporter_options=exporter_options,
61+
)
62+
self.assertTrue(result, "configure() should return True with exporter_options")
63+
64+
@patch("microsoft_agents_a365.observability.core.config.Agent365Exporter")
65+
@patch("microsoft_agents_a365.observability.core.config.BatchSpanProcessor")
66+
@patch("microsoft_agents_a365.observability.core.config.is_agent365_exporter_enabled")
67+
def test_batch_span_processor_and_exporter_called_with_correct_values(
68+
self, mock_is_enabled, mock_batch_processor, mock_exporter
69+
):
70+
"""Test that BatchSpanProcessor and Agent365Exporter are called with correct values from exporter_options."""
71+
# Enable Agent365 exporter for this test
72+
mock_is_enabled.return_value = True
73+
74+
# Create exporter options with specific values
75+
exporter_options = Agent365ExporterOptions(
76+
cluster_category="staging",
77+
token_resolver=self.mock_token_resolver,
78+
use_s2s_endpoint=True,
79+
max_queue_size=512,
80+
scheduled_delay_ms=1000,
81+
exporter_timeout_ms=10000,
82+
max_export_batch_size=128,
83+
)
84+
85+
# Configure with exporter_options
86+
result = configure(
87+
service_name="test-service",
88+
service_namespace="test-namespace",
89+
exporter_options=exporter_options,
90+
)
91+
92+
# Verify configuration succeeded
93+
self.assertTrue(result, "configure() should return True")
94+
95+
# Verify Agent365Exporter was called with correct parameters
96+
mock_exporter.assert_called_once_with(
97+
token_resolver=self.mock_token_resolver,
98+
cluster_category="staging",
99+
use_s2s_endpoint=True,
17100
)
18-
print("✅ configure() executed successfully")
19-
except Exception as e:
20-
print(f"❌ configure() failed: {e}")
21-
return False
22101

23-
# Test SpanProcessor class
24-
try:
25-
SpanProcessor()
26-
print("✅ SpanProcessor created successfully")
27-
except Exception as e:
28-
print(f"❌ SpanProcessor creation failed: {e}")
29-
return False
102+
# Verify BatchSpanProcessor was called with correct parameters from exporter_options
103+
mock_batch_processor.assert_called_once()
104+
call_args = mock_batch_processor.call_args
105+
self.assertEqual(call_args.kwargs["max_queue_size"], 512)
106+
self.assertEqual(call_args.kwargs["schedule_delay_millis"], 1000)
107+
self.assertEqual(call_args.kwargs["export_timeout_millis"], 10000)
108+
self.assertEqual(call_args.kwargs["max_export_batch_size"], 128)
30109

31-
print("✅ All tests passed!")
32-
return True
110+
def test_span_processor_creation(self):
111+
"""Test SpanProcessor class creation."""
112+
processor = SpanProcessor()
113+
self.assertIsNotNone(processor, "SpanProcessor should be created successfully")
33114

34115

35116
if __name__ == "__main__":
36-
test_basic_functionality()
117+
unittest.main()

tests/observability/core/test_baggage_builder.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
GEN_AI_AGENT_ID_KEY,
1212
GEN_AI_AGENT_UPN_KEY,
1313
GEN_AI_CALLER_ID_KEY,
14+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY,
15+
GEN_AI_EXECUTION_SOURCE_NAME_KEY,
1416
HIRING_MANAGER_ID_KEY,
1517
OPERATION_SOURCE_KEY,
18+
SESSION_DESCRIPTION_KEY,
19+
SESSION_ID_KEY,
1620
TENANT_ID_KEY,
1721
)
1822
from microsoft_agents_a365.observability.core.middleware.baggage_builder import BaggageBuilder
@@ -48,6 +52,9 @@ def setUp(self):
4852
# Clear any existing context/baggage before each test
4953
context.detach(context.attach({}))
5054

55+
# Create a fresh BaggageBuilder for each test
56+
self.builder = BaggageBuilder()
57+
5158
def tearDown(self):
5259
"""Clean up after each test."""
5360
# Clear context
@@ -280,6 +287,79 @@ def fake_from_turn_context(turn_ctx: any):
280287
# Restore original
281288
tempBaggageBuilder.from_turn_context = original_fn
282289

290+
def test_source_metadata_name_method(self):
291+
"""Test deprecated source_metadata_name method - should delegate to channel_name."""
292+
# Should exist and be callable
293+
self.assertTrue(hasattr(self.builder, "source_metadata_name"))
294+
self.assertTrue(callable(self.builder.source_metadata_name))
295+
296+
# Should set channel name baggage through delegation
297+
with self.builder.source_metadata_name("test-channel").build():
298+
current_baggage = baggage.get_all()
299+
self.assertEqual(current_baggage.get(GEN_AI_EXECUTION_SOURCE_NAME_KEY), "test-channel")
300+
301+
def test_source_metadata_description_method(self):
302+
"""Test deprecated source_metadata_description method - should delegate to channel_links."""
303+
# Should exist and be callable
304+
self.assertTrue(hasattr(self.builder, "source_metadata_description"))
305+
self.assertTrue(callable(self.builder.source_metadata_description))
306+
307+
# Should set channel description baggage through delegation
308+
with self.builder.source_metadata_description("test-description").build():
309+
current_baggage = baggage.get_all()
310+
self.assertEqual(
311+
current_baggage.get(GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY), "test-description"
312+
)
313+
314+
def test_session_id_method(self):
315+
"""Test session_id method sets session ID baggage."""
316+
# Should exist and be callable
317+
self.assertTrue(hasattr(self.builder, "session_id"))
318+
self.assertTrue(callable(self.builder.session_id))
319+
320+
# Should set session ID baggage
321+
with self.builder.session_id("test-session-123").build():
322+
current_baggage = baggage.get_all()
323+
self.assertEqual(current_baggage.get(SESSION_ID_KEY), "test-session-123")
324+
325+
def test_session_description_method(self):
326+
"""Test session_description method sets session description baggage."""
327+
# Should exist and be callable
328+
self.assertTrue(hasattr(self.builder, "session_description"))
329+
self.assertTrue(callable(self.builder.session_description))
330+
331+
# Should set session description baggage
332+
with self.builder.session_description("test session description").build():
333+
current_baggage = baggage.get_all()
334+
self.assertEqual(
335+
current_baggage.get(SESSION_DESCRIPTION_KEY), "test session description"
336+
)
337+
338+
def test_channel_name_method(self):
339+
"""Test channel_name method sets channel name baggage."""
340+
# Should exist and be callable
341+
self.assertTrue(hasattr(self.builder, "channel_name"))
342+
self.assertTrue(callable(self.builder.channel_name))
343+
344+
# Should set channel name baggage
345+
with self.builder.channel_name("Teams Channel").build():
346+
current_baggage = baggage.get_all()
347+
self.assertEqual(current_baggage.get(GEN_AI_EXECUTION_SOURCE_NAME_KEY), "Teams Channel")
348+
349+
def test_channel_links_method(self):
350+
"""Test channel_links method sets channel description baggage."""
351+
# Should exist and be callable
352+
self.assertTrue(hasattr(self.builder, "channel_links"))
353+
self.assertTrue(callable(self.builder.channel_links))
354+
355+
# Should set channel description baggage
356+
with self.builder.channel_links("https://teams.microsoft.com/channel/123").build():
357+
current_baggage = baggage.get_all()
358+
self.assertEqual(
359+
current_baggage.get(GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY),
360+
"https://teams.microsoft.com/channel/123",
361+
)
362+
283363

284364
if __name__ == "__main__":
285365
unittest.main()

tests/observability/core/test_invoke_agent_scope.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,25 @@
66

77
from microsoft_agents_a365.observability.core import (
88
AgentDetails,
9+
ExecutionType,
910
InvokeAgentDetails,
1011
InvokeAgentScope,
12+
Request,
13+
SourceMetadata,
1114
TenantDetails,
1215
configure,
1316
)
17+
from microsoft_agents_a365.observability.core.models.caller_details import CallerDetails
18+
from opentelemetry import trace
19+
from opentelemetry.sdk.trace import TracerProvider
20+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
21+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
22+
23+
# Constants for span attribute keys
24+
GEN_AI_EXECUTION_SOURCE_NAME_KEY = "gen_ai.channel.name"
25+
GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY = "gen_ai.channel.link"
26+
GEN_AI_EXECUTION_TYPE_KEY = "gen_ai.execution.type"
27+
GEN_AI_INPUT_MESSAGES_KEY = "gen_ai.input.messages"
1428

1529

1630
class TestInvokeAgentScope(unittest.TestCase):
@@ -37,6 +51,42 @@ def setUpClass(cls):
3751
session_id="session-123",
3852
)
3953

54+
# Create source metadata for requests
55+
cls.source_metadata = SourceMetadata(
56+
id="source-agent-456",
57+
name="Source Channel",
58+
icon_uri="https://example.com/source-icon.png",
59+
description="Source channel description",
60+
)
61+
62+
# Create a comprehensive request object
63+
cls.test_request = Request(
64+
content="Process customer inquiry about order status",
65+
execution_type=ExecutionType.AGENT_TO_AGENT,
66+
session_id="session-abc123",
67+
source_metadata=cls.source_metadata,
68+
)
69+
70+
# Create caller details (non-agentic caller)
71+
cls.caller_details = CallerDetails(
72+
caller_id="user-123",
73+
caller_upn="user@contoso.com",
74+
caller_name="John Doe",
75+
caller_user_id="user-id-456",
76+
tenant_id="tenant-789",
77+
)
78+
79+
# Create caller agent details (agentic caller)
80+
cls.caller_agent_details = AgentDetails(
81+
agent_id="caller-agent-789",
82+
agent_name="Caller Agent",
83+
agent_description="The agent that initiated this request",
84+
agent_blueprint_id="blueprint-456",
85+
agent_auid="auid-123",
86+
agent_upn="agent@contoso.com",
87+
tenant_id="tenant-789",
88+
)
89+
4090
def test_record_response_method_exists(self):
4191
"""Test that record_response method exists on InvokeAgentScope."""
4292
scope = InvokeAgentScope.start(self.invoke_details, self.tenant_details)
@@ -67,6 +117,62 @@ def test_record_output_messages_method_exists(self):
67117
self.assertTrue(callable(scope.record_output_messages))
68118
scope.dispose()
69119

120+
def test_request_attributes_set_on_span(self):
121+
"""Test that request parameters from mock data are available on span attributes."""
122+
# Set up tracer to capture spans
123+
span_exporter = InMemorySpanExporter()
124+
tracer_provider = TracerProvider()
125+
tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
126+
trace.set_tracer_provider(tracer_provider)
127+
128+
# Create scope with request
129+
scope = InvokeAgentScope.start(
130+
invoke_agent_details=self.invoke_details,
131+
tenant_details=self.tenant_details,
132+
request=self.test_request,
133+
)
134+
135+
if scope is not None:
136+
scope.dispose()
137+
138+
# Check if mock data parameters are available in span attributes
139+
finished_spans = span_exporter.get_finished_spans()
140+
141+
if finished_spans:
142+
# Get attributes from the span
143+
span = finished_spans[-1]
144+
span_attributes = getattr(span, "attributes", {}) or {}
145+
146+
# Verify mock data request parameters are in span attributes
147+
# Check source channel name from mock data
148+
if GEN_AI_EXECUTION_SOURCE_NAME_KEY in span_attributes:
149+
self.assertEqual(
150+
span_attributes[GEN_AI_EXECUTION_SOURCE_NAME_KEY],
151+
self.source_metadata.name, # From cls.source_metadata.name
152+
)
153+
154+
# Check source channel description from mock data
155+
if GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY in span_attributes:
156+
self.assertEqual(
157+
span_attributes[GEN_AI_EXECUTION_SOURCE_DESCRIPTION_KEY],
158+
self.source_metadata.description, # From cls.source_metadata.description
159+
)
160+
161+
# Check execution type from mock data
162+
if GEN_AI_EXECUTION_TYPE_KEY in span_attributes:
163+
self.assertEqual(
164+
span_attributes[GEN_AI_EXECUTION_TYPE_KEY],
165+
self.test_request.execution_type.value, # From cls.test_request.execution_type
166+
)
167+
168+
# Check input messages contain request content from mock data
169+
if GEN_AI_INPUT_MESSAGES_KEY in span_attributes:
170+
input_messages = span_attributes[GEN_AI_INPUT_MESSAGES_KEY]
171+
self.assertIn(
172+
self.test_request.content, # From cls.test_request.content
173+
input_messages,
174+
)
175+
70176

71177
if __name__ == "__main__":
72178
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)