184 lines
7.2 KiB
Python
184 lines
7.2 KiB
Python
"""Tests for the AnthropicMessagesTransport.
|
|
|
|
Behavioral tests that require the real anthropic transport implementation.
|
|
"""
|
|
|
|
import json
|
|
import pytest
|
|
from types import SimpleNamespace
|
|
|
|
from agent.transports import get_transport
|
|
from agent.transports.types import NormalizedResponse
|
|
|
|
|
|
@pytest.fixture
|
|
def transport():
|
|
"""Load the real Anthropic transport by registering the plugin."""
|
|
from hermes_agent_anthropic import register as _anthro_register
|
|
from agent.plugin_registries import registries
|
|
|
|
class _Ctx:
|
|
def register_transport(self, api_mode, obj):
|
|
from agent.transports import register_transport
|
|
register_transport(api_mode, obj)
|
|
def register_provider_resolver(self, name, fn):
|
|
registries.register_provider_resolver(name, fn)
|
|
def register_provider_services(self, name, services):
|
|
registries.register_provider_services(name, services)
|
|
def register_credential_pool_hook(self, name, hook):
|
|
registries.register_credential_pool_hook(name, hook)
|
|
def register_pricing_provider(self, name, entries):
|
|
registries.register_pricing_provider(name, entries)
|
|
def register_provider_overlay(self, entry):
|
|
registries.register_provider_overlay(entry)
|
|
def __getattr__(self, name):
|
|
if name.startswith("register_"):
|
|
return lambda *a, **kw: None
|
|
raise AttributeError(name)
|
|
|
|
_anthro_register(_Ctx())
|
|
return get_transport("anthropic_messages")
|
|
|
|
|
|
|
|
class TestAnthropicTransportBehavioral:
|
|
|
|
# (fixture defined at module level above)
|
|
|
|
def test_api_mode(self, transport):
|
|
assert transport.api_mode == "anthropic_messages"
|
|
|
|
def test_convert_tools_simple(self, transport):
|
|
tools = [{
|
|
"type": "function",
|
|
"function": {
|
|
"name": "test_tool",
|
|
"description": "A test",
|
|
"parameters": {"type": "object", "properties": {}},
|
|
}
|
|
}]
|
|
result = transport.convert_tools(tools)
|
|
assert len(result) == 1
|
|
assert result[0]["name"] == "test_tool"
|
|
assert "input_schema" in result[0]
|
|
|
|
def test_validate_response_none(self, transport):
|
|
assert transport.validate_response(None) is False
|
|
|
|
def test_validate_response_empty_content(self, transport):
|
|
r = SimpleNamespace(content=[])
|
|
assert transport.validate_response(r) is False
|
|
|
|
def test_validate_response_empty_content_with_end_turn_is_valid(self, transport):
|
|
r = SimpleNamespace(content=[], stop_reason="end_turn")
|
|
assert transport.validate_response(r) is True
|
|
|
|
def test_validate_response_empty_content_with_tool_use_is_invalid(self, transport):
|
|
r = SimpleNamespace(content=[], stop_reason="tool_use")
|
|
assert transport.validate_response(r) is False
|
|
|
|
def test_validate_response_valid(self, transport):
|
|
r = SimpleNamespace(content=[SimpleNamespace(type="text", text="hello")])
|
|
assert transport.validate_response(r) is True
|
|
|
|
def test_map_finish_reason(self, transport):
|
|
assert transport.map_finish_reason("end_turn") == "stop"
|
|
assert transport.map_finish_reason("tool_use") == "tool_calls"
|
|
assert transport.map_finish_reason("max_tokens") == "length"
|
|
assert transport.map_finish_reason("stop_sequence") == "stop"
|
|
assert transport.map_finish_reason("refusal") == "content_filter"
|
|
assert transport.map_finish_reason("model_context_window_exceeded") == "length"
|
|
assert transport.map_finish_reason("unknown") == "stop"
|
|
|
|
def test_extract_cache_stats_none_usage(self, transport):
|
|
r = SimpleNamespace(usage=None)
|
|
assert transport.extract_cache_stats(r) is None
|
|
|
|
def test_extract_cache_stats_with_cache(self, transport):
|
|
usage = SimpleNamespace(cache_read_input_tokens=100, cache_creation_input_tokens=50)
|
|
r = SimpleNamespace(usage=usage)
|
|
result = transport.extract_cache_stats(r)
|
|
assert result == {"cached_tokens": 100, "creation_tokens": 50}
|
|
|
|
def test_extract_cache_stats_zero(self, transport):
|
|
usage = SimpleNamespace(cache_read_input_tokens=0, cache_creation_input_tokens=0)
|
|
r = SimpleNamespace(usage=usage)
|
|
assert transport.extract_cache_stats(r) is None
|
|
|
|
def test_normalize_response_text(self, transport):
|
|
"""Test normalization of a simple text response."""
|
|
r = SimpleNamespace(
|
|
content=[SimpleNamespace(type="text", text="Hello world")],
|
|
stop_reason="end_turn",
|
|
usage=SimpleNamespace(input_tokens=10, output_tokens=5),
|
|
model="claude-sonnet-4-6",
|
|
)
|
|
nr = transport.normalize_response(r)
|
|
assert isinstance(nr, NormalizedResponse)
|
|
assert nr.content == "Hello world"
|
|
assert nr.tool_calls is None or nr.tool_calls == []
|
|
assert nr.finish_reason == "stop"
|
|
|
|
def test_normalize_response_tool_calls(self, transport):
|
|
"""Test normalization of a tool-use response."""
|
|
r = SimpleNamespace(
|
|
content=[
|
|
SimpleNamespace(
|
|
type="tool_use",
|
|
id="toolu_123",
|
|
name="terminal",
|
|
input={"command": "ls"},
|
|
),
|
|
],
|
|
stop_reason="tool_use",
|
|
usage=SimpleNamespace(input_tokens=10, output_tokens=20),
|
|
model="claude-sonnet-4-6",
|
|
)
|
|
nr = transport.normalize_response(r)
|
|
assert nr.finish_reason == "tool_calls"
|
|
assert len(nr.tool_calls) == 1
|
|
tc = nr.tool_calls[0]
|
|
assert tc.name == "terminal"
|
|
assert tc.id == "toolu_123"
|
|
assert '"command"' in tc.arguments
|
|
|
|
def test_normalize_response_thinking(self, transport):
|
|
"""Test normalization preserves thinking content."""
|
|
r = SimpleNamespace(
|
|
content=[
|
|
SimpleNamespace(type="thinking", thinking="Let me think..."),
|
|
SimpleNamespace(type="text", text="The answer is 42"),
|
|
],
|
|
stop_reason="end_turn",
|
|
usage=SimpleNamespace(input_tokens=10, output_tokens=15),
|
|
model="claude-sonnet-4-6",
|
|
)
|
|
nr = transport.normalize_response(r)
|
|
assert nr.content == "The answer is 42"
|
|
assert nr.reasoning == "Let me think..."
|
|
|
|
def test_build_kwargs_returns_dict(self, transport):
|
|
"""Test build_kwargs produces a usable kwargs dict."""
|
|
messages = [{"role": "user", "content": "Hello"}]
|
|
kw = transport.build_kwargs(
|
|
model="claude-sonnet-4-6",
|
|
messages=messages,
|
|
max_tokens=1024,
|
|
)
|
|
assert isinstance(kw, dict)
|
|
assert "model" in kw
|
|
assert "max_tokens" in kw
|
|
assert "messages" in kw
|
|
|
|
def test_convert_messages_extracts_system(self, transport):
|
|
"""Test convert_messages separates system from messages."""
|
|
messages = [
|
|
{"role": "system", "content": "You are helpful."},
|
|
{"role": "user", "content": "Hi"},
|
|
]
|
|
system, msgs = transport.convert_messages(messages)
|
|
# System should be extracted
|
|
assert system is not None
|
|
# Messages should only have user
|
|
assert len(msgs) >= 1
|