"""Telemetry module unit tests.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from agentkit.telemetry.tracer import ( NoOpSpan, NoOpTracer, OTelSpan, OTelTracer, TelemetryConfig, get_tracer, init_telemetry, ) from agentkit.quality.alignment import AlignmentGuard, AlignmentConfig # ── NoOpSpan 测试 ────────────────────────────────────────── class TestNoOpSpan: """NoOpSpan no-op 行为测试""" def test_is_recording_returns_false(self): span = NoOpSpan() assert span.is_recording() is False def test_set_attribute_does_nothing(self): span = NoOpSpan() # 不应抛出异常 span.set_attribute("key", "value") def test_add_event_does_nothing(self): span = NoOpSpan() span.add_event("event_name", {"attr": "val"}) def test_record_exception_does_nothing(self): span = NoOpSpan() span.record_exception(ValueError("test")) def test_context_manager(self): span = NoOpSpan() with span as s: assert s is span # ── NoOpTracer 测试 ──────────────────────────────────────── class TestNoOpTracer: """NoOpTracer no-op 行为测试""" def test_start_span_returns_noop_span(self): tracer = NoOpTracer() span = tracer.start_span("test.span") assert isinstance(span, NoOpSpan) def test_start_as_current_span_returns_noop_span(self): tracer = NoOpTracer() span = tracer.start_as_current_span("test.span") assert isinstance(span, NoOpSpan) def test_start_span_with_attributes(self): tracer = NoOpTracer() span = tracer.start_span("test.span", attributes={"key": "value"}) assert isinstance(span, NoOpSpan) def test_start_span_as_context_manager(self): tracer = NoOpTracer() with tracer.start_span("test.span") as span: assert isinstance(span, NoOpSpan) # ── TelemetryConfig 测试 ─────────────────────────────────── class TestTelemetryConfig: """TelemetryConfig 默认值测试""" def test_default_values(self): config = TelemetryConfig() assert config.enabled is False assert config.otlp_endpoint == "" assert config.service_name == "agentkit" assert config.sample_rate == 1.0 def test_custom_values(self): config = TelemetryConfig( enabled=True, otlp_endpoint="http://localhost:4317", service_name="my-service", sample_rate=0.5, ) assert config.enabled is True assert config.otlp_endpoint == "http://localhost:4317" assert config.service_name == "my-service" assert config.sample_rate == 0.5 # ── init_telemetry 测试 ──────────────────────────────────── class TestInitTelemetry: """init_telemetry 初始化测试""" def test_disabled_returns_noop_tracer(self): config = TelemetryConfig(enabled=False) init_telemetry(config) tracer = get_tracer() assert isinstance(tracer, NoOpTracer) def test_missing_opentelemetry_falls_back_to_noop(self): """当 opentelemetry 包未安装时,优雅降级为 NoOpTracer""" config = TelemetryConfig(enabled=True, otlp_endpoint="http://localhost:4317") # 即使 opentelemetry 未安装,也不应抛出异常 init_telemetry(config) tracer = get_tracer() # 在没有 opentelemetry 的环境中应为 NoOpTracer assert isinstance(tracer, (NoOpTracer, OTelTracer)) def test_init_with_exception_falls_back_to_noop(self): """初始化过程中出现异常时,降级为 NoOpTracer""" config = TelemetryConfig(enabled=True, otlp_endpoint="http://localhost:4317") with patch( "agentkit.telemetry.tracer.init_telemetry", side_effect=Exception("init error"), ) as mock_init: # init_telemetry 被模拟为抛异常,但实际调用的是原始函数 # 这里验证的是 init_telemetry 本身不会崩溃 pass # 直接调用,不应崩溃 init_telemetry(config) tracer = get_tracer() assert isinstance(tracer, (NoOpTracer, OTelTracer)) # ── get_tracer 测试 ──────────────────────────────────────── class TestGetTracer: """get_tracer 全局实例测试""" def test_returns_global_tracer(self): # 确保先重置为 NoOpTracer init_telemetry(TelemetryConfig(enabled=False)) tracer = get_tracer() assert isinstance(tracer, NoOpTracer) def test_get_tracer_returns_same_instance(self): init_telemetry(TelemetryConfig(enabled=False)) tracer1 = get_tracer() tracer2 = get_tracer() assert tracer1 is tracer2 # ── AlignmentGuard span 测试 ────────────────────────────── class TestAlignmentGuardSpan: """AlignmentGuard 创建 span 并设置属性""" @pytest.mark.asyncio async def test_guard_creates_span_with_attributes(self): """对齐检查时创建 span 并设置 guard.passed 和 guard.checked_by 属性""" init_telemetry(TelemetryConfig(enabled=False)) tracer = get_tracer() mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) mock_span.__exit__ = MagicMock(return_value=False) mock_span.set_attribute = MagicMock() with patch.object(tracer, "start_span", return_value=mock_span): config = AlignmentConfig(constraints=["forbidden_word"]) guard = AlignmentGuard(config) result = await guard.check_output( {"content": "This contains forbidden_word"} ) tracer.start_span.assert_called_once_with("guard.check") call_args_list = [ (call.args[0], call.args[1]) for call in mock_span.set_attribute.call_args_list ] assert ("guard.constraints_count", 1) in call_args_list assert ("guard.passed", False) in call_args_list assert ("guard.checked_by", "rule") in call_args_list @pytest.mark.asyncio async def test_guard_span_on_pass(self): """对齐检查通过时 span 设置 guard.passed=True""" init_telemetry(TelemetryConfig(enabled=False)) tracer = get_tracer() mock_span = MagicMock() mock_span.__enter__ = MagicMock(return_value=mock_span) mock_span.__exit__ = MagicMock(return_value=False) mock_span.set_attribute = MagicMock() with patch.object(tracer, "start_span", return_value=mock_span): config = AlignmentConfig(constraints=["forbidden_word"]) guard = AlignmentGuard(config) result = await guard.check_output( {"content": "This is clean text"} ) call_args_list = [ (call.args[0], call.args[1]) for call in mock_span.set_attribute.call_args_list ] assert ("guard.passed", True) in call_args_list assert ("guard.checked_by", "rule") in call_args_list