Skip to content

Commit 5f87535

Browse files
committed
add test for custom wrappers
1 parent a18fe1d commit 5f87535

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
"""
2+
Tests for OpenAI API Custom Wrappers
3+
4+
This module contains tests for the custom wrappers used in the OpenAI instrumentor.
5+
It verifies that our custom wrappers correctly handle context from OpenAI Agents SDK
6+
and set the appropriate attributes on spans.
7+
"""
8+
9+
import pytest
10+
from unittest.mock import MagicMock, patch
11+
12+
from opentelemetry import context as context_api
13+
from opentelemetry.trace import SpanKind, StatusCode
14+
15+
from agentops.instrumentation.openai.instrumentor import (
16+
OpenAIInstrumentor,
17+
responses_wrapper,
18+
async_responses_wrapper,
19+
)
20+
21+
22+
class TestOpenAICustomWrappers:
23+
"""Tests for OpenAI API custom wrappers"""
24+
25+
@pytest.fixture
26+
def mock_tracer(self):
27+
"""Set up a mock tracer for testing"""
28+
mock_tracer = MagicMock()
29+
mock_span = MagicMock()
30+
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
31+
return mock_tracer, mock_span
32+
33+
@pytest.fixture
34+
def mock_context(self):
35+
"""Set up a mock context with OpenAI Agents SDK trace information"""
36+
# Mock the context_api.get_value method to return our test values
37+
with patch.object(context_api, "get_value") as mock_get_value:
38+
# Set up the mock to return different values based on the key
39+
def side_effect(key, default=None, context=None):
40+
if key == "openai_agents.trace_id":
41+
return "test-trace-id"
42+
elif key == "openai_agents.parent_id":
43+
return "test-parent-id"
44+
elif key == "openai_agents.workflow_input":
45+
return "test workflow input"
46+
elif key == "suppress_instrumentation":
47+
return False
48+
return default
49+
50+
mock_get_value.side_effect = side_effect
51+
yield mock_get_value
52+
53+
def test_responses_wrapper_with_context(self, mock_tracer, mock_context):
54+
"""Test that the responses_wrapper correctly handles context from OpenAI Agents SDK"""
55+
mock_tracer, mock_span = mock_tracer
56+
57+
# Create a mock wrapped function
58+
mock_wrapped = MagicMock()
59+
mock_wrapped.return_value = {"id": "test-response-id", "model": "test-model"}
60+
61+
# Set up mock get_response_attributes to return empty dict
62+
with patch("agentops.instrumentation.openai.instrumentor.get_response_attributes", return_value={}):
63+
# Call the wrapper
64+
result = responses_wrapper(mock_tracer, mock_wrapped, None, [], {})
65+
66+
# Verify the wrapped function was called
67+
assert mock_wrapped.called
68+
69+
# Verify the span was created with the correct name and kind
70+
mock_tracer.start_as_current_span.assert_called_once_with(
71+
"openai.responses.create",
72+
kind=SpanKind.CLIENT,
73+
)
74+
75+
# Verify the context attributes were set on the span
76+
mock_span.set_attribute.assert_any_call("openai_agents.trace_id", "test-trace-id")
77+
mock_span.set_attribute.assert_any_call("openai_agents.parent_id", "test-parent-id")
78+
mock_span.set_attribute.assert_any_call("workflow.input", "test workflow input")
79+
80+
# Verify the status was set to OK
81+
# Use assert_called to check that set_status was called, then check the status code
82+
assert mock_span.set_status.called, "set_status was not called"
83+
status_arg = mock_span.set_status.call_args[0][0]
84+
assert status_arg.status_code == StatusCode.OK, f"Expected status code OK, got {status_arg.status_code}"
85+
86+
# Verify the result was returned
87+
assert result == {"id": "test-response-id", "model": "test-model"}
88+
89+
@pytest.mark.asyncio
90+
async def test_async_responses_wrapper_with_context(self, mock_tracer, mock_context):
91+
"""Test that the async_responses_wrapper correctly handles context from OpenAI Agents SDK"""
92+
mock_tracer, mock_span = mock_tracer
93+
94+
# Create a mock wrapped function that returns a coroutine
95+
async def mock_async_func(*args, **kwargs):
96+
return {"id": "test-response-id", "model": "test-model"}
97+
98+
mock_wrapped = MagicMock()
99+
mock_wrapped.side_effect = mock_async_func
100+
101+
# Set up mock get_response_attributes to return empty dict
102+
with patch("agentops.instrumentation.openai.instrumentor.get_response_attributes", return_value={}):
103+
# Call the wrapper
104+
result = await async_responses_wrapper(mock_tracer, mock_wrapped, None, [], {})
105+
106+
# Verify the wrapped function was called
107+
assert mock_wrapped.called
108+
109+
# Verify the span was created with the correct name and kind
110+
mock_tracer.start_as_current_span.assert_called_once_with(
111+
"openai.responses.create",
112+
kind=SpanKind.CLIENT,
113+
)
114+
115+
# Verify the context attributes were set on the span
116+
mock_span.set_attribute.assert_any_call("openai_agents.trace_id", "test-trace-id")
117+
mock_span.set_attribute.assert_any_call("openai_agents.parent_id", "test-parent-id")
118+
mock_span.set_attribute.assert_any_call("workflow.input", "test workflow input")
119+
120+
# Verify the status was set to OK
121+
# Use assert_called to check that set_status was called, then check the status code
122+
assert mock_span.set_status.called, "set_status was not called"
123+
status_arg = mock_span.set_status.call_args[0][0]
124+
assert status_arg.status_code == StatusCode.OK, f"Expected status code OK, got {status_arg.status_code}"
125+
126+
# Verify the result was returned
127+
assert result == {"id": "test-response-id", "model": "test-model"}
128+
129+
def test_instrumentor_uses_custom_wrappers(self):
130+
"""Test that the instrumentor uses the custom wrappers"""
131+
# Create instrumentor
132+
instrumentor = OpenAIInstrumentor()
133+
134+
# Mock wrap_function_wrapper
135+
with patch("wrapt.wrap_function_wrapper") as mock_wrap_function_wrapper:
136+
# Mock wrap to avoid errors
137+
with patch("agentops.instrumentation.openai.instrumentor.wrap"):
138+
# Mock the parent class's _instrument method to do nothing
139+
with patch.object(instrumentor.__class__.__bases__[0], "_instrument"):
140+
# Instrument
141+
instrumentor._instrument(tracer_provider=MagicMock())
142+
143+
# Verify wrap_function_wrapper was called for both methods
144+
assert mock_wrap_function_wrapper.call_count == 2
145+
146+
# Check the first call arguments for Responses.create
147+
first_call_args = mock_wrap_function_wrapper.call_args_list[0]
148+
assert first_call_args[0][0] == "openai.resources.responses"
149+
assert first_call_args[0][1] == "Responses.create"
150+
151+
# Check the second call arguments for AsyncResponses.create
152+
second_call_args = mock_wrap_function_wrapper.call_args_list[1]
153+
assert second_call_args[0][0] == "openai.resources.responses"
154+
assert second_call_args[0][1] == "AsyncResponses.create"
155+
156+
def test_instrumentor_unwraps_custom_wrappers(self):
157+
"""Test that the instrumentor unwraps the custom wrappers"""
158+
# Create instrumentor
159+
instrumentor = OpenAIInstrumentor()
160+
161+
# Mock unwrap
162+
with patch("opentelemetry.instrumentation.utils.unwrap") as mock_unwrap:
163+
# Mock standard unwrap to avoid errors
164+
with patch("agentops.instrumentation.openai.instrumentor.unwrap"):
165+
# Mock the parent class's _uninstrument method to do nothing
166+
with patch.object(instrumentor.__class__.__bases__[0], "_uninstrument"):
167+
# Uninstrument
168+
instrumentor._uninstrument()
169+
170+
# Verify unwrap was called for both methods
171+
assert mock_unwrap.call_count == 2
172+
173+
# Check the first call arguments for Responses.create
174+
first_call_args = mock_unwrap.call_args_list[0]
175+
assert first_call_args[0][0] == "openai.resources.responses.Responses"
176+
assert first_call_args[0][1] == "create"
177+
178+
# Check the second call arguments for AsyncResponses.create
179+
second_call_args = mock_unwrap.call_args_list[1]
180+
assert second_call_args[0][0] == "openai.resources.responses.AsyncResponses"
181+
assert second_call_args[0][1] == "create"

0 commit comments

Comments
 (0)