Files
2026-07-13 13:22:34 +08:00

138 lines
4.3 KiB
Python

import sys
from unittest.mock import MagicMock, patch
import pytest
from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock, UserMessage
import mlflow.anthropic
from mlflow.anthropic.autolog import patched_claude_sdk_init
def test_anthropic_autolog_without_claude_sdk():
sys.modules.pop("claude_agent_sdk", None)
with (
patch.dict(
"sys.modules",
{
"anthropic": MagicMock(__version__="0.35.0"),
"anthropic.resources": MagicMock(Messages=MagicMock, AsyncMessages=MagicMock),
},
),
patch("mlflow.anthropic.safe_patch"),
):
mlflow.anthropic.autolog()
def _patch_sdk_init(mock_self, response_messages):
original_init = MagicMock()
async def fake_receive_response():
for msg in response_messages:
yield msg
mock_self.receive_response = fake_receive_response
patched_claude_sdk_init(original_init, mock_self)
return original_init
def test_patched_claude_sdk_init_wraps_receive_response():
mock_self = MagicMock()
async def fake_receive_response():
yield "msg1"
mock_self.receive_response = fake_receive_response
original_init = MagicMock()
patched_claude_sdk_init(original_init, mock_self)
original_init.assert_called_once_with(mock_self, None)
assert mock_self.receive_response is not fake_receive_response
@pytest.mark.asyncio
async def test_receive_response_builds_trace():
mock_self = MagicMock()
messages = [
UserMessage(content="Hello"),
AssistantMessage(content=[TextBlock(text="Hi!")], model="claude-sonnet-4-20250514"),
ResultMessage(
subtype="success",
duration_ms=5000,
duration_api_ms=4000,
is_error=False,
num_turns=1,
session_id="test-session",
usage={"input_tokens": 100, "output_tokens": 20},
),
]
_patch_sdk_init(mock_self, messages)
with (
patch("mlflow.utils.autologging_utils.autologging_is_disabled", return_value=False),
patch("mlflow.claude_code.tracing.process_sdk_messages") as mock_process,
):
[msg async for msg in mock_self.receive_response()]
mock_process.assert_called_once()
called_messages = mock_process.call_args[0][0]
assert len(called_messages) == 3
result_messages = [m for m in called_messages if isinstance(m, ResultMessage)]
assert len(result_messages) == 1
assert result_messages[0].usage == {"input_tokens": 100, "output_tokens": 20}
@pytest.mark.asyncio
async def test_query_captures_async_generator_prompt():
mock_self = MagicMock()
async def fake_query(prompt, *args, **kwargs):
# Consume the generator like the real SDK would
async for _ in prompt:
pass
mock_self.query = fake_query
response_messages = [
AssistantMessage(content=[TextBlock(text="Hi!")], model="claude-sonnet-4-20250514"),
ResultMessage(
subtype="success",
duration_ms=1000,
duration_api_ms=800,
is_error=False,
num_turns=1,
session_id="s",
),
]
_patch_sdk_init(mock_self, response_messages)
async def prompt_generator():
yield {"type": "user", "message": {"role": "user", "content": "Hello from generator"}}
with (
patch("mlflow.utils.autologging_utils.autologging_is_disabled", return_value=False),
patch("mlflow.claude_code.tracing.process_sdk_messages") as mock_process,
):
await mock_self.query(prompt_generator())
[msg async for msg in mock_self.receive_response()]
mock_process.assert_called_once()
called_messages = mock_process.call_args[0][0]
user_messages = [m for m in called_messages if isinstance(m, UserMessage)]
assert len(user_messages) == 1
assert user_messages[0].content == "Hello from generator"
@pytest.mark.asyncio
async def test_receive_response_skips_when_autologging_disabled():
mock_self = MagicMock()
_patch_sdk_init(mock_self, ["msg1", "msg2"])
with (
patch("mlflow.utils.autologging_utils.autologging_is_disabled", return_value=True),
patch("mlflow.claude_code.tracing.process_sdk_messages") as mock_process,
):
[msg async for msg in mock_self.receive_response()]
mock_process.assert_not_called()