import logging from typing import Any, cast from agents.tracing.processor_interface import TracingProcessor from agents.tracing.scope import Scope from agents.tracing.spans import Span from agents.tracing.traces import NoOpTrace, Trace, TraceImpl, TraceState, reattach_trace class DummyProcessor(TracingProcessor): def __init__(self) -> None: self.started: list[str] = [] self.ended: list[str] = [] def on_trace_start(self, trace: Trace) -> None: self.started.append(trace.trace_id) def on_trace_end(self, trace: Trace) -> None: self.ended.append(trace.trace_id) def on_span_start(self, span: Span[Any]) -> None: return None def on_span_end(self, span: Span[Any]) -> None: return None def shutdown(self) -> None: return None def force_flush(self) -> None: return None def test_no_op_trace_double_enter_logs_error(caplog) -> None: Scope.set_current_trace(None) trace = NoOpTrace() with caplog.at_level(logging.ERROR): trace.start() trace.__enter__() trace.__enter__() # Second entry should log missing context token error assert trace._started is True trace.__exit__(None, None, None) def test_trace_impl_lifecycle_sets_scope() -> None: Scope.set_current_trace(None) processor = DummyProcessor() trace = TraceImpl( name="test-trace", trace_id="trace-123", group_id="group-1", metadata={"k": "v"}, processor=processor, ) assert Scope.get_current_trace() is None with trace as current: assert current.trace_id == "trace-123" assert Scope.get_current_trace() is trace assert processor.started == ["trace-123"] assert processor.ended == ["trace-123"] assert Scope.get_current_trace() is None assert trace.export() == { "object": "trace", "id": "trace-123", "workflow_name": "test-trace", "group_id": "group-1", "metadata": {"k": "v"}, } def test_trace_impl_double_start_and_finish_without_start(caplog) -> None: Scope.set_current_trace(None) processor = DummyProcessor() trace = TraceImpl( name="double-start", trace_id=None, group_id=None, metadata=None, processor=processor, ) trace.start() trace.start() # should no-op when already started trace.finish(reset_current=True) with caplog.at_level(logging.ERROR): trace._started = True trace._prev_context_token = None trace.__enter__() # logs when started but no context token trace.finish(reset_current=True) fresh = TraceImpl( name="finish-no-start", trace_id=None, group_id=None, metadata=None, processor=processor, ) fresh.finish(reset_current=True) # should not raise when never started def test_reattached_trace_restores_scope_without_reemitting_processor_events() -> None: Scope.set_current_trace(None) processor = DummyProcessor() original = TraceImpl( name="test-trace", trace_id="trace-123", group_id="group-1", metadata={"k": "v"}, processor=processor, ) with original: pass restored = reattach_trace(cast(TraceState, TraceState.from_trace(original))) assert restored is not None with restored as current: assert current.trace_id == "trace-123" assert Scope.get_current_trace() is restored assert processor.started == ["trace-123"] assert processor.ended == ["trace-123"] assert Scope.get_current_trace() is None