Skip to content

Commit 3ebaa96

Browse files
acailicclaude
andcommitted
test: add 94 tests for untested high-risk modules
Cover alert system (42), comparison routes (25), simple SDK (15), and live monitor (12). Zero regressions, full suite green at 1904. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd11099 commit 3ebaa96

4 files changed

Lines changed: 2014 additions & 0 deletions

File tree

tests/sdk/test_simple.py

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
"""Tests for the simplified high-level API (trace decorator, trace_session)."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import AsyncMock, MagicMock, patch
6+
7+
import pytest
8+
9+
from agent_debugger_sdk.simple import _ensure_initialized, trace, trace_session
10+
11+
12+
class TestEnsureInitialized:
13+
"""Tests for _ensure_initialized idempotency."""
14+
15+
def test_ensure_initialized_calls_init_once(self):
16+
"""_ensure_initialized should call init() exactly once on multiple calls."""
17+
import agent_debugger_sdk.simple as simple_mod
18+
19+
# Reset the flag
20+
simple_mod._initialized = False
21+
22+
with patch("agent_debugger_sdk.simple.init") as mock_init:
23+
# First call should invoke init
24+
_ensure_initialized()
25+
assert mock_init.call_count == 1
26+
assert simple_mod._initialized is True
27+
28+
# Second call should be no-op
29+
_ensure_initialized()
30+
assert mock_init.call_count == 1 # Still 1, not 2
31+
assert simple_mod._initialized is True
32+
33+
def test_ensure_initialized_idempotent(self):
34+
"""Multiple calls to _ensure_initialized should only initialize once."""
35+
import agent_debugger_sdk.simple as simple_mod
36+
37+
simple_mod._initialized = False
38+
39+
with patch("agent_debugger_sdk.simple.init") as mock_init:
40+
for _ in range(5):
41+
_ensure_initialized()
42+
43+
assert mock_init.call_count == 1
44+
assert simple_mod._initialized is True
45+
46+
47+
class TestTraceDecorator:
48+
"""Tests for the @trace decorator."""
49+
50+
@pytest.mark.asyncio
51+
async def test_trace_decorator_bare_wraps_function(self):
52+
"""@trace without arguments should wrap and execute the function."""
53+
import agent_debugger_sdk.simple as simple_mod
54+
55+
simple_mod._initialized = False
56+
57+
with patch("agent_debugger_sdk.simple.init"):
58+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
59+
mock_ctx = AsyncMock()
60+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
61+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
62+
mock_ctx._session_start_event = None
63+
MockCtx.return_value = mock_ctx
64+
65+
@trace
66+
async def my_agent(prompt: str) -> str:
67+
return f"response to {prompt}"
68+
69+
result = await my_agent("hello")
70+
assert result == "response to hello"
71+
assert my_agent.__name__ == "my_agent"
72+
73+
@pytest.mark.asyncio
74+
async def test_trace_decorator_with_custom_name_and_framework(self):
75+
"""@trace(name=..., framework=...) should use provided values."""
76+
import agent_debugger_sdk.simple as simple_mod
77+
78+
simple_mod._initialized = False
79+
80+
with patch("agent_debugger_sdk.simple.init"):
81+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
82+
mock_ctx = AsyncMock()
83+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
84+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
85+
mock_ctx._session_start_event = None
86+
MockCtx.return_value = mock_ctx
87+
88+
@trace(name="custom_name", framework="test_framework")
89+
async def my_agent(prompt: str) -> str:
90+
return "ok"
91+
92+
result = await my_agent("test")
93+
assert result == "ok"
94+
95+
# Verify TraceContext was called with correct args
96+
MockCtx.assert_called_once_with(
97+
agent_name="custom_name",
98+
framework="test_framework",
99+
)
100+
101+
@pytest.mark.asyncio
102+
async def test_trace_decorator_uses_qualname_as_default(self):
103+
"""@trace should use function __qualname__ when name is not provided."""
104+
import agent_debugger_sdk.simple as simple_mod
105+
106+
simple_mod._initialized = False
107+
108+
with patch("agent_debugger_sdk.simple.init"):
109+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
110+
mock_ctx = AsyncMock()
111+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
112+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
113+
mock_ctx._session_start_event = None
114+
MockCtx.return_value = mock_ctx
115+
116+
@trace
117+
async def my_function() -> str:
118+
return "result"
119+
120+
await my_function()
121+
122+
# Should use __qualname__ as agent name (includes full path)
123+
assert MockCtx.call_count == 1
124+
call_kwargs = MockCtx.call_args.kwargs
125+
assert "my_function" in call_kwargs["agent_name"]
126+
assert call_kwargs["framework"] == "custom"
127+
128+
@pytest.mark.asyncio
129+
async def test_trace_decorator_with_session_start_event(self):
130+
"""@trace should set parent when session_start_event exists."""
131+
import agent_debugger_sdk.simple as simple_mod
132+
133+
simple_mod._initialized = False
134+
135+
with patch("agent_debugger_sdk.simple.init"):
136+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
137+
mock_ctx = AsyncMock()
138+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
139+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
140+
# Simulate having a session start event
141+
mock_event = MagicMock()
142+
mock_event.id = "session-start-123"
143+
mock_ctx._session_start_event = mock_event
144+
mock_ctx.set_parent = MagicMock()
145+
MockCtx.return_value = mock_ctx
146+
147+
@trace
148+
async def my_agent() -> str:
149+
return "done"
150+
151+
await my_agent()
152+
153+
# Should set parent to session start event ID
154+
mock_ctx.set_parent.assert_called_once_with("session-start-123")
155+
156+
157+
class TestTraceSession:
158+
"""Tests for the trace_session() context manager."""
159+
160+
@pytest.mark.asyncio
161+
async def test_trace_session_yields_context(self):
162+
"""trace_session() should yield a TraceContext."""
163+
import agent_debugger_sdk.simple as simple_mod
164+
165+
simple_mod._initialized = False
166+
167+
with patch("agent_debugger_sdk.simple.init"):
168+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
169+
mock_ctx = AsyncMock()
170+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
171+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
172+
mock_ctx._session_start_event = None
173+
MockCtx.return_value = mock_ctx
174+
175+
async with trace_session("test_agent") as ctx:
176+
assert ctx is mock_ctx
177+
178+
# Verify TraceContext was created with correct args
179+
MockCtx.assert_called_once_with(
180+
agent_name="test_agent",
181+
framework="custom",
182+
session_id=None,
183+
tags=None,
184+
)
185+
186+
@pytest.mark.asyncio
187+
async def test_trace_session_with_custom_parameters(self):
188+
"""trace_session() should accept and pass custom parameters."""
189+
import agent_debugger_sdk.simple as simple_mod
190+
191+
simple_mod._initialized = False
192+
193+
with patch("agent_debugger_sdk.simple.init"):
194+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
195+
mock_ctx = AsyncMock()
196+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
197+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
198+
mock_ctx._session_start_event = None
199+
MockCtx.return_value = mock_ctx
200+
201+
async with trace_session(
202+
agent_name="custom_agent",
203+
framework="langchain",
204+
session_id="custom-123",
205+
tags=["test", "demo"],
206+
) as ctx:
207+
assert ctx is mock_ctx
208+
209+
# Verify all parameters were passed
210+
MockCtx.assert_called_once_with(
211+
agent_name="custom_agent",
212+
framework="langchain",
213+
session_id="custom-123",
214+
tags=["test", "demo"],
215+
)
216+
217+
@pytest.mark.asyncio
218+
async def test_trace_session_can_record_events(self):
219+
"""trace_session() context should allow recording events."""
220+
import agent_debugger_sdk.simple as simple_mod
221+
222+
simple_mod._initialized = False
223+
224+
with patch("agent_debugger_sdk.simple.init"):
225+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
226+
mock_ctx = AsyncMock()
227+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
228+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
229+
mock_ctx._session_start_event = None
230+
mock_ctx.record_decision = AsyncMock(return_value="event-123")
231+
MockCtx.return_value = mock_ctx
232+
233+
async with trace_session("agent") as ctx:
234+
event_id = await ctx.record_decision(
235+
reasoning="test reasoning",
236+
confidence=0.9,
237+
chosen_action="test_action",
238+
)
239+
assert event_id == "event-123"
240+
241+
# Verify record_decision was called
242+
mock_ctx.record_decision.assert_called_once_with(
243+
reasoning="test reasoning",
244+
confidence=0.9,
245+
chosen_action="test_action",
246+
)
247+
248+
@pytest.mark.asyncio
249+
async def test_trace_session_with_session_start_event(self):
250+
"""trace_session() should set parent when session_start_event exists."""
251+
import agent_debugger_sdk.simple as simple_mod
252+
253+
simple_mod._initialized = False
254+
255+
with patch("agent_debugger_sdk.simple.init"):
256+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
257+
mock_ctx = AsyncMock()
258+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
259+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
260+
# Simulate having a session start event
261+
mock_event = MagicMock()
262+
mock_event.id = "session-start-456"
263+
mock_ctx._session_start_event = mock_event
264+
mock_ctx.set_parent = MagicMock()
265+
MockCtx.return_value = mock_ctx
266+
267+
async with trace_session("agent") as ctx:
268+
assert ctx is mock_ctx
269+
270+
# Should set parent to session start event ID
271+
mock_ctx.set_parent.assert_called_once_with("session-start-456")
272+
273+
@pytest.mark.asyncio
274+
async def test_trace_session_handles_exceptions(self):
275+
"""trace_session() should propagate exceptions properly."""
276+
import agent_debugger_sdk.simple as simple_mod
277+
278+
simple_mod._initialized = False
279+
280+
with patch("agent_debugger_sdk.simple.init"):
281+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
282+
mock_ctx = AsyncMock()
283+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
284+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
285+
mock_ctx._session_start_event = None
286+
MockCtx.return_value = mock_ctx
287+
288+
with pytest.raises(ValueError, match="test error"):
289+
async with trace_session("agent"):
290+
raise ValueError("test error")
291+
292+
# Verify __aexit__ was called with exception info
293+
mock_ctx.__aexit__.assert_called_once()
294+
exit_args = mock_ctx.__aexit__.call_args[0]
295+
# exc_type, exc_value, traceback
296+
assert exit_args[0] is ValueError
297+
assert isinstance(exit_args[1], ValueError)
298+
assert str(exit_args[1]) == "test error"
299+
300+
301+
class TestIntegration:
302+
"""Integration tests for simple API behavior."""
303+
304+
def test_init_called_on_first_trace_use(self):
305+
"""init() should be called when @trace is first used."""
306+
import agent_debugger_sdk.simple as simple_mod
307+
308+
simple_mod._initialized = False
309+
310+
with patch("agent_debugger_sdk.simple.init") as mock_init:
311+
# Create a traced function
312+
@trace
313+
async def agent() -> str:
314+
return "ok"
315+
316+
# init should be called during decoration
317+
assert mock_init.call_count == 1
318+
assert simple_mod._initialized is True
319+
320+
@pytest.mark.asyncio
321+
async def test_init_called_on_first_trace_session_use(self):
322+
"""init() should be called when trace_session() is first used."""
323+
import agent_debugger_sdk.simple as simple_mod
324+
325+
simple_mod._initialized = False
326+
327+
with patch("agent_debugger_sdk.simple.init") as mock_init:
328+
with patch("agent_debugger_sdk.core.context.TraceContext") as MockCtx:
329+
mock_ctx = AsyncMock()
330+
mock_ctx.__aenter__ = AsyncMock(return_value=mock_ctx)
331+
mock_ctx.__aexit__ = AsyncMock(return_value=False)
332+
mock_ctx._session_start_event = None
333+
MockCtx.return_value = mock_ctx
334+
335+
# trace_session is a generator, so init is called when we enter the context
336+
async with trace_session("agent"):
337+
# init should be called during context entry
338+
assert mock_init.call_count == 1
339+
assert simple_mod._initialized is True
340+
341+
def test_multiple_trace_decorators_share_initialization(self):
342+
"""Multiple @trace decorators should share a single initialization."""
343+
import agent_debugger_sdk.simple as simple_mod
344+
345+
simple_mod._initialized = False
346+
347+
with patch("agent_debugger_sdk.simple.init") as mock_init:
348+
@trace
349+
async def agent1() -> str:
350+
return "1"
351+
352+
@trace
353+
async def agent2() -> str:
354+
return "2"
355+
356+
# init should only be called once
357+
assert mock_init.call_count == 1
358+
assert simple_mod._initialized is True
359+
360+
def test_trace_and_trace_session_share_initialization(self):
361+
"""@trace and trace_session() should share a single initialization."""
362+
import agent_debugger_sdk.simple as simple_mod
363+
364+
simple_mod._initialized = False
365+
366+
with patch("agent_debugger_sdk.simple.init") as mock_init:
367+
@trace
368+
async def agent() -> str:
369+
return "ok"
370+
371+
with patch("agent_debugger_sdk.core.context.TraceContext"):
372+
trace_session("agent")
373+
374+
# init should only be called once across both
375+
assert mock_init.call_count == 1
376+
assert simple_mod._initialized is True

0 commit comments

Comments
 (0)