# -*- coding: utf-8 -*- """ Unit tests for Anthropic Pydantic models. Comprehensive tests for all Anthropic API models: - Content blocks (text, image, tool_use, tool_result, thinking) - Image sources (base64, URL) - Messages and requests - Tools and tool choice - Responses and streaming events - Error models """ import pytest from pydantic import ValidationError from kiro.models_anthropic import ( # Content blocks TextContentBlock, ThinkingContentBlock, ToolUseContentBlock, ToolResultContentBlock, # Image models Base64ImageSource, URLImageSource, ImageContentBlock, ContentBlock, # Message models AnthropicMessage, # Tool models AnthropicTool, ToolChoiceAuto, ToolChoiceAny, ToolChoiceTool, ToolChoice, # Request models SystemContentBlock, AnthropicMessagesRequest, # Response models AnthropicUsage, AnthropicMessagesResponse, # Streaming models MessageStartEvent, ContentBlockStartEvent, TextDelta, ThinkingDelta, InputJsonDelta, ContentBlockDeltaEvent, ContentBlockStopEvent, MessageDeltaUsage, MessageDeltaEvent, MessageStopEvent, PingEvent, ErrorEvent, # Error models AnthropicErrorDetail, AnthropicErrorResponse, ) # Base64 1x1 pixel JPEG for testing TEST_IMAGE_BASE64 = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AVN//2Q==" # ================================================================================================== # Tests for Base64ImageSource # ================================================================================================== class TestBase64ImageSource: """Tests for Base64ImageSource Pydantic model.""" def test_valid_base64_source(self): """ What it does: Verifies creation of valid Base64ImageSource. Purpose: Ensure model accepts valid base64 image data. """ print("Setup: Creating Base64ImageSource with valid data...") source = Base64ImageSource( type="base64", media_type="image/jpeg", data=TEST_IMAGE_BASE64 ) print(f"Result: {source}") print(f"Comparing type: Expected 'base64', Got '{source.type}'") assert source.type == "base64" print(f"Comparing media_type: Expected 'image/jpeg', Got '{source.media_type}'") assert source.media_type == "image/jpeg" print(f"Comparing data: Expected {TEST_IMAGE_BASE64[:20]}..., Got {source.data[:20]}...") assert source.data == TEST_IMAGE_BASE64 def test_type_defaults_to_base64(self): """ What it does: Verifies that type defaults to "base64". Purpose: Ensure default value is set correctly. """ print("Setup: Creating Base64ImageSource without explicit type...") source = Base64ImageSource( media_type="image/png", data=TEST_IMAGE_BASE64 ) print(f"Comparing type: Expected 'base64', Got '{source.type}'") assert source.type == "base64" def test_requires_media_type(self): """ What it does: Verifies that media_type is required. Purpose: Ensure validation fails without media_type. """ print("Setup: Attempting to create Base64ImageSource without media_type...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: Base64ImageSource(data=TEST_IMAGE_BASE64) print(f"ValidationError raised: {exc_info.value}") assert "media_type" in str(exc_info.value) def test_requires_data(self): """ What it does: Verifies that data is required. Purpose: Ensure validation fails without data. """ print("Setup: Attempting to create Base64ImageSource without data...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: Base64ImageSource(media_type="image/jpeg") print(f"ValidationError raised: {exc_info.value}") assert "data" in str(exc_info.value) def test_accepts_various_media_types(self): """ What it does: Verifies acceptance of various image media types. Purpose: Ensure all common image formats are supported. """ print("Setup: Testing various media types...") media_types = ["image/jpeg", "image/png", "image/gif", "image/webp"] for media_type in media_types: print(f"Testing media_type: {media_type}") source = Base64ImageSource(media_type=media_type, data=TEST_IMAGE_BASE64) assert source.media_type == media_type print("All media types accepted successfully") # ================================================================================================== # Tests for URLImageSource # ================================================================================================== class TestURLImageSource: """Tests for URLImageSource Pydantic model.""" def test_valid_url_source(self): """ What it does: Verifies creation of valid URLImageSource. Purpose: Ensure model accepts valid URL. """ print("Setup: Creating URLImageSource with valid URL...") source = URLImageSource( type="url", url="https://example.com/image.jpg" ) print(f"Result: {source}") print(f"Comparing type: Expected 'url', Got '{source.type}'") assert source.type == "url" print(f"Comparing url: Expected 'https://example.com/image.jpg', Got '{source.url}'") assert source.url == "https://example.com/image.jpg" def test_type_defaults_to_url(self): """ What it does: Verifies that type defaults to "url". Purpose: Ensure default value is set correctly. """ print("Setup: Creating URLImageSource without explicit type...") source = URLImageSource(url="https://example.com/image.png") print(f"Comparing type: Expected 'url', Got '{source.type}'") assert source.type == "url" def test_requires_url(self): """ What it does: Verifies that url is required. Purpose: Ensure validation fails without url. """ print("Setup: Attempting to create URLImageSource without url...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: URLImageSource() print(f"ValidationError raised: {exc_info.value}") assert "url" in str(exc_info.value) # ================================================================================================== # Tests for ImageContentBlock # ================================================================================================== class TestImageContentBlock: """Tests for ImageContentBlock Pydantic model.""" def test_with_base64_source(self): """ What it does: Verifies creation of ImageContentBlock with base64 source. Purpose: Ensure model accepts Base64ImageSource. """ print("Setup: Creating ImageContentBlock with base64 source...") block = ImageContentBlock( type="image", source=Base64ImageSource( media_type="image/jpeg", data=TEST_IMAGE_BASE64 ) ) print(f"Result: {block}") print(f"Comparing type: Expected 'image', Got '{block.type}'") assert block.type == "image" print(f"Comparing source.type: Expected 'base64', Got '{block.source.type}'") assert block.source.type == "base64" assert block.source.media_type == "image/jpeg" def test_with_url_source(self): """ What it does: Verifies creation of ImageContentBlock with URL source. Purpose: Ensure model accepts URLImageSource. """ print("Setup: Creating ImageContentBlock with URL source...") block = ImageContentBlock( type="image", source=URLImageSource(url="https://example.com/image.jpg") ) print(f"Result: {block}") print(f"Comparing type: Expected 'image', Got '{block.type}'") assert block.type == "image" print(f"Comparing source.type: Expected 'url', Got '{block.source.type}'") assert block.source.type == "url" assert block.source.url == "https://example.com/image.jpg" def test_with_dict_base64_source(self): """ What it does: Verifies creation of ImageContentBlock with dict source. Purpose: Ensure model accepts dict that matches Base64ImageSource schema. """ print("Setup: Creating ImageContentBlock with dict source...") block = ImageContentBlock( type="image", source={ "type": "base64", "media_type": "image/png", "data": TEST_IMAGE_BASE64 } ) print(f"Result: {block}") print(f"Comparing source.type: Expected 'base64', Got '{block.source.type}'") assert block.source.type == "base64" assert block.source.media_type == "image/png" def test_with_dict_url_source(self): """ What it does: Verifies creation of ImageContentBlock with dict URL source. Purpose: Ensure model accepts dict that matches URLImageSource schema. """ print("Setup: Creating ImageContentBlock with dict URL source...") block = ImageContentBlock( type="image", source={ "type": "url", "url": "https://example.com/test.gif" } ) print(f"Result: {block}") print(f"Comparing source.type: Expected 'url', Got '{block.source.type}'") assert block.source.type == "url" assert block.source.url == "https://example.com/test.gif" def test_type_literal_is_image(self): """ What it does: Verifies that type must be "image". Purpose: Ensure type literal validation works. """ print("Setup: Creating ImageContentBlock with correct type...") block = ImageContentBlock( source=Base64ImageSource(media_type="image/jpeg", data=TEST_IMAGE_BASE64) ) print(f"Comparing type: Expected 'image', Got '{block.type}'") assert block.type == "image" def test_requires_source(self): """ What it does: Verifies that source is required. Purpose: Ensure validation fails without source. """ print("Setup: Attempting to create ImageContentBlock without source...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ImageContentBlock(type="image") print(f"ValidationError raised: {exc_info.value}") assert "source" in str(exc_info.value) # ================================================================================================== # Tests for ContentBlock Union # ================================================================================================== class TestContentBlockUnion: """Tests for ContentBlock union type accepting ImageContentBlock.""" def test_accepts_text_content_block(self): """ What it does: Verifies ContentBlock accepts TextContentBlock. Purpose: Ensure union includes text blocks. """ print("Setup: Creating TextContentBlock...") block: ContentBlock = TextContentBlock(text="Hello, world!") print(f"Result: {block}") print(f"Comparing type: Expected 'text', Got '{block.type}'") assert block.type == "text" assert block.text == "Hello, world!" def test_accepts_image_content_block(self): """ What it does: Verifies ContentBlock accepts ImageContentBlock. Purpose: Ensure union includes image blocks (Issue #30 fix). This is the key test that verifies the fix for Issue #30. Before the fix, ContentBlock union did not include ImageContentBlock, causing 422 Validation Error when image content was sent. """ print("Setup: Creating ImageContentBlock...") block: ContentBlock = ImageContentBlock( source=Base64ImageSource(media_type="image/jpeg", data=TEST_IMAGE_BASE64) ) print(f"Result: {block}") print(f"Comparing type: Expected 'image', Got '{block.type}'") assert block.type == "image" assert block.source.type == "base64" def test_accepts_tool_use_content_block(self): """ What it does: Verifies ContentBlock accepts ToolUseContentBlock. Purpose: Ensure union includes tool_use blocks. """ print("Setup: Creating ToolUseContentBlock...") block: ContentBlock = ToolUseContentBlock( id="call_123", name="get_weather", input={"location": "Moscow"} ) print(f"Result: {block}") print(f"Comparing type: Expected 'tool_use', Got '{block.type}'") assert block.type == "tool_use" def test_accepts_tool_result_content_block(self): """ What it does: Verifies ContentBlock accepts ToolResultContentBlock. Purpose: Ensure union includes tool_result blocks. """ print("Setup: Creating ToolResultContentBlock...") block: ContentBlock = ToolResultContentBlock( tool_use_id="call_123", content="Weather: Sunny, 25°C" ) print(f"Result: {block}") print(f"Comparing type: Expected 'tool_result', Got '{block.type}'") assert block.type == "tool_result" # ================================================================================================== # Tests for AnthropicMessage with Image Content (Issue #30 fix verification) # ================================================================================================== class TestAnthropicMessageWithImages: """ Tests for AnthropicMessage with image content. These tests verify the fix for Issue #30 - 422 Validation Error when sending image content blocks in messages. """ def test_message_with_image_content_validates(self): """ What it does: Verifies AnthropicMessage accepts image content blocks. Purpose: This is the PRIMARY test for Issue #30 fix. Before the fix, this would raise a ValidationError because ContentBlock union did not include ImageContentBlock. """ print("Setup: Creating AnthropicMessage with image content...") message = AnthropicMessage( role="user", content=[ TextContentBlock(text="What's in this image?"), ImageContentBlock( source=Base64ImageSource( media_type="image/jpeg", data=TEST_IMAGE_BASE64 ) ) ] ) print(f"Result: {message}") print(f"Comparing role: Expected 'user', Got '{message.role}'") assert message.role == "user" print(f"Comparing content length: Expected 2, Got {len(message.content)}") assert len(message.content) == 2 print(f"Comparing content[0].type: Expected 'text', Got '{message.content[0].type}'") assert message.content[0].type == "text" print(f"Comparing content[1].type: Expected 'image', Got '{message.content[1].type}'") assert message.content[1].type == "image" def test_message_with_dict_image_content_validates(self): """ What it does: Verifies AnthropicMessage accepts dict image content. Purpose: Ensure raw dict format (as received from API) validates correctly. This is how the actual API request comes in - as raw dicts, not Pydantic models. """ print("Setup: Creating AnthropicMessage with dict image content...") message = AnthropicMessage( role="user", content=[ {"type": "text", "text": "Describe this image"}, { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": TEST_IMAGE_BASE64 } } ] ) print(f"Result: {message}") print(f"Comparing content length: Expected 2, Got {len(message.content)}") assert len(message.content) == 2 print(f"Comparing content[1].type: Expected 'image', Got '{message.content[1].type}'") assert message.content[1].type == "image" assert message.content[1].source.type == "base64" def test_message_with_multiple_images_validates(self): """ What it does: Verifies AnthropicMessage accepts multiple images. Purpose: Ensure multiple image blocks in one message work correctly. """ print("Setup: Creating AnthropicMessage with multiple images...") message = AnthropicMessage( role="user", content=[ {"type": "text", "text": "Compare these images"}, { "type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": TEST_IMAGE_BASE64} }, { "type": "image", "source": {"type": "base64", "media_type": "image/png", "data": TEST_IMAGE_BASE64} }, { "type": "image", "source": {"type": "base64", "media_type": "image/webp", "data": TEST_IMAGE_BASE64} } ] ) print(f"Result content length: {len(message.content)}") assert len(message.content) == 4 image_blocks = [b for b in message.content if b.type == "image"] print(f"Image blocks count: {len(image_blocks)}") assert len(image_blocks) == 3 def test_message_with_url_image_validates(self): """ What it does: Verifies AnthropicMessage accepts URL image source. Purpose: Ensure URL-based images are accepted (even if not fully supported). """ print("Setup: Creating AnthropicMessage with URL image...") message = AnthropicMessage( role="user", content=[ {"type": "text", "text": "What's in this image?"}, { "type": "image", "source": { "type": "url", "url": "https://example.com/image.jpg" } } ] ) print(f"Result: {message}") print(f"Comparing content[1].source.type: Expected 'url', Got '{message.content[1].source.type}'") assert message.content[1].source.type == "url" assert message.content[1].source.url == "https://example.com/image.jpg" # ================================================================================================== # Tests for AnthropicMessagesRequest with Image Content # ================================================================================================== class TestAnthropicMessagesRequestWithImages: """Tests for full AnthropicMessagesRequest with image content.""" def test_request_with_image_message_validates(self): """ What it does: Verifies full request with image content validates. Purpose: End-to-end validation test for Issue #30 fix. This simulates the actual request that was failing with 422 error. """ print("Setup: Creating full AnthropicMessagesRequest with image...") request = AnthropicMessagesRequest( model="claude-sonnet-4-5", max_tokens=1024, messages=[ AnthropicMessage( role="user", content=[ {"type": "text", "text": "What's in this image?"}, { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": TEST_IMAGE_BASE64 } } ] ) ] ) print(f"Result: {request}") print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{request.model}'") assert request.model == "claude-sonnet-4-5" print(f"Comparing messages count: Expected 1, Got {len(request.messages)}") assert len(request.messages) == 1 print(f"Comparing content count: Expected 2, Got {len(request.messages[0].content)}") assert len(request.messages[0].content) == 2 print("Request with image content validated successfully!") def test_request_with_conversation_including_images(self): """ What it does: Verifies multi-turn conversation with images validates. Purpose: Ensure images work in conversation context. """ print("Setup: Creating multi-turn conversation with images...") request = AnthropicMessagesRequest( model="claude-sonnet-4-5", max_tokens=1024, messages=[ AnthropicMessage( role="user", content=[ {"type": "text", "text": "What's in this image?"}, { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": TEST_IMAGE_BASE64 } } ] ), AnthropicMessage( role="assistant", content="I can see a small test image." ), AnthropicMessage( role="user", content="Can you describe it in more detail?" ) ] ) print(f"Result messages count: {len(request.messages)}") assert len(request.messages) == 3 # First message has image assert request.messages[0].content[1].type == "image" # Second message is string (assistant) assert request.messages[1].content == "I can see a small test image." # Third message is string (user follow-up) assert request.messages[2].content == "Can you describe it in more detail?" print("Multi-turn conversation with images validated successfully!") # ================================================================================================== # Tests for TextContentBlock # ================================================================================================== class TestTextContentBlock: """Tests for TextContentBlock Pydantic model.""" def test_valid_text_block(self): """ What it does: Verifies creation of valid TextContentBlock. Purpose: Ensure model accepts valid text content. """ print("Setup: Creating TextContentBlock with valid text...") block = TextContentBlock(text="Hello, world!") print(f"Result: {block}") print(f"Comparing type: Expected 'text', Got '{block.type}'") assert block.type == "text" print(f"Comparing text: Expected 'Hello, world!', Got '{block.text}'") assert block.text == "Hello, world!" def test_type_defaults_to_text(self): """ What it does: Verifies that type defaults to "text". Purpose: Ensure default value is set correctly. """ print("Setup: Creating TextContentBlock without explicit type...") block = TextContentBlock(text="Test") print(f"Comparing type: Expected 'text', Got '{block.type}'") assert block.type == "text" def test_requires_text(self): """ What it does: Verifies that text is required. Purpose: Ensure validation fails without text. """ print("Setup: Attempting to create TextContentBlock without text...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: TextContentBlock() print(f"ValidationError raised: {exc_info.value}") assert "text" in str(exc_info.value) def test_accepts_empty_string(self): """ What it does: Verifies that empty string is accepted. Purpose: Ensure empty text is valid. """ print("Setup: Creating TextContentBlock with empty string...") block = TextContentBlock(text="") print(f"Comparing text: Expected '', Got '{block.text}'") assert block.text == "" def test_accepts_multiline_text(self): """ What it does: Verifies that multiline text is accepted. Purpose: Ensure newlines are preserved. """ print("Setup: Creating TextContentBlock with multiline text...") multiline = "Line 1\nLine 2\nLine 3" block = TextContentBlock(text=multiline) print(f"Comparing text: Expected multiline, Got '{block.text}'") assert block.text == multiline assert "\n" in block.text # ================================================================================================== # Tests for ThinkingContentBlock # ================================================================================================== class TestThinkingContentBlock: """Tests for ThinkingContentBlock Pydantic model.""" def test_valid_thinking_block(self): """ What it does: Verifies creation of valid ThinkingContentBlock. Purpose: Ensure model accepts valid thinking content. """ print("Setup: Creating ThinkingContentBlock with valid thinking...") block = ThinkingContentBlock( thinking="Let me analyze this step by step...", signature="abc123" ) print(f"Result: {block}") print(f"Comparing type: Expected 'thinking', Got '{block.type}'") assert block.type == "thinking" print(f"Comparing thinking: Got '{block.thinking[:30]}...'") assert block.thinking == "Let me analyze this step by step..." print(f"Comparing signature: Expected 'abc123', Got '{block.signature}'") assert block.signature == "abc123" def test_type_defaults_to_thinking(self): """ What it does: Verifies that type defaults to "thinking". Purpose: Ensure default value is set correctly. """ print("Setup: Creating ThinkingContentBlock without explicit type...") block = ThinkingContentBlock(thinking="Test thinking") print(f"Comparing type: Expected 'thinking', Got '{block.type}'") assert block.type == "thinking" def test_signature_defaults_to_empty(self): """ What it does: Verifies that signature defaults to empty string. Purpose: Ensure default value is set correctly. """ print("Setup: Creating ThinkingContentBlock without signature...") block = ThinkingContentBlock(thinking="Test") print(f"Comparing signature: Expected '', Got '{block.signature}'") assert block.signature == "" def test_requires_thinking(self): """ What it does: Verifies that thinking is required. Purpose: Ensure validation fails without thinking. """ print("Setup: Attempting to create ThinkingContentBlock without thinking...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ThinkingContentBlock() print(f"ValidationError raised: {exc_info.value}") assert "thinking" in str(exc_info.value) # ================================================================================================== # Tests for ToolUseContentBlock # ================================================================================================== class TestToolUseContentBlock: """Tests for ToolUseContentBlock Pydantic model.""" def test_valid_tool_use_block(self): """ What it does: Verifies creation of valid ToolUseContentBlock. Purpose: Ensure model accepts valid tool use data. """ print("Setup: Creating ToolUseContentBlock with valid data...") block = ToolUseContentBlock( id="call_123", name="get_weather", input={"location": "Moscow", "units": "celsius"} ) print(f"Result: {block}") print(f"Comparing type: Expected 'tool_use', Got '{block.type}'") assert block.type == "tool_use" print(f"Comparing id: Expected 'call_123', Got '{block.id}'") assert block.id == "call_123" print(f"Comparing name: Expected 'get_weather', Got '{block.name}'") assert block.name == "get_weather" print(f"Comparing input: Got {block.input}") assert block.input == {"location": "Moscow", "units": "celsius"} def test_type_defaults_to_tool_use(self): """ What it does: Verifies that type defaults to "tool_use". Purpose: Ensure default value is set correctly. """ print("Setup: Creating ToolUseContentBlock without explicit type...") block = ToolUseContentBlock(id="call_1", name="test", input={}) print(f"Comparing type: Expected 'tool_use', Got '{block.type}'") assert block.type == "tool_use" def test_requires_id(self): """ What it does: Verifies that id is required. Purpose: Ensure validation fails without id. """ print("Setup: Attempting to create ToolUseContentBlock without id...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ToolUseContentBlock(name="test", input={}) print(f"ValidationError raised: {exc_info.value}") assert "id" in str(exc_info.value) def test_requires_name(self): """ What it does: Verifies that name is required. Purpose: Ensure validation fails without name. """ print("Setup: Attempting to create ToolUseContentBlock without name...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ToolUseContentBlock(id="call_1", input={}) print(f"ValidationError raised: {exc_info.value}") assert "name" in str(exc_info.value) def test_requires_input(self): """ What it does: Verifies that input is required. Purpose: Ensure validation fails without input. """ print("Setup: Attempting to create ToolUseContentBlock without input...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ToolUseContentBlock(id="call_1", name="test") print(f"ValidationError raised: {exc_info.value}") assert "input" in str(exc_info.value) def test_accepts_empty_input(self): """ What it does: Verifies that empty input dict is accepted. Purpose: Ensure tools without parameters work. """ print("Setup: Creating ToolUseContentBlock with empty input...") block = ToolUseContentBlock(id="call_1", name="no_params_tool", input={}) print(f"Comparing input: Expected {{}}, Got {block.input}") assert block.input == {} def test_accepts_complex_input(self): """ What it does: Verifies that complex nested input is accepted. Purpose: Ensure nested structures work. """ print("Setup: Creating ToolUseContentBlock with complex input...") complex_input = { "query": "test", "options": {"limit": 10, "offset": 0}, "filters": ["active", "recent"] } block = ToolUseContentBlock(id="call_1", name="search", input=complex_input) print(f"Comparing input: Got {block.input}") assert block.input == complex_input # ================================================================================================== # Tests for ToolResultContentBlock # ================================================================================================== class TestToolResultContentBlock: """Tests for ToolResultContentBlock Pydantic model.""" def test_valid_tool_result_block(self): """ What it does: Verifies creation of valid ToolResultContentBlock. Purpose: Ensure model accepts valid tool result data. """ print("Setup: Creating ToolResultContentBlock with valid data...") block = ToolResultContentBlock( tool_use_id="call_123", content="Weather in Moscow: Sunny, 25°C" ) print(f"Result: {block}") print(f"Comparing type: Expected 'tool_result', Got '{block.type}'") assert block.type == "tool_result" print(f"Comparing tool_use_id: Expected 'call_123', Got '{block.tool_use_id}'") assert block.tool_use_id == "call_123" print(f"Comparing content: Got '{block.content}'") assert block.content == "Weather in Moscow: Sunny, 25°C" def test_type_defaults_to_tool_result(self): """ What it does: Verifies that type defaults to "tool_result". Purpose: Ensure default value is set correctly. """ print("Setup: Creating ToolResultContentBlock without explicit type...") block = ToolResultContentBlock(tool_use_id="call_1") print(f"Comparing type: Expected 'tool_result', Got '{block.type}'") assert block.type == "tool_result" def test_requires_tool_use_id(self): """ What it does: Verifies that tool_use_id is required. Purpose: Ensure validation fails without tool_use_id. """ print("Setup: Attempting to create ToolResultContentBlock without tool_use_id...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ToolResultContentBlock(content="Result") print(f"ValidationError raised: {exc_info.value}") assert "tool_use_id" in str(exc_info.value) def test_content_is_optional(self): """ What it does: Verifies that content is optional. Purpose: Ensure tool results without content work. """ print("Setup: Creating ToolResultContentBlock without content...") block = ToolResultContentBlock(tool_use_id="call_1") print(f"Comparing content: Expected None, Got {block.content}") assert block.content is None def test_accepts_list_content(self): """ What it does: Verifies that list content is accepted. Purpose: Ensure content can be list of TextContentBlock. """ print("Setup: Creating ToolResultContentBlock with list content...") block = ToolResultContentBlock( tool_use_id="call_1", content=[TextContentBlock(text="Part 1"), TextContentBlock(text="Part 2")] ) print(f"Comparing content type: Expected list, Got {type(block.content)}") assert isinstance(block.content, list) assert len(block.content) == 2 def test_is_error_field(self): """ What it does: Verifies that is_error field works. Purpose: Ensure error results can be marked. """ print("Setup: Creating ToolResultContentBlock with is_error=True...") block = ToolResultContentBlock( tool_use_id="call_1", content="Error: File not found", is_error=True ) print(f"Comparing is_error: Expected True, Got {block.is_error}") assert block.is_error is True def test_is_error_defaults_to_none(self): """ What it does: Verifies that is_error defaults to None. Purpose: Ensure default value is correct. """ print("Setup: Creating ToolResultContentBlock without is_error...") block = ToolResultContentBlock(tool_use_id="call_1", content="Success") print(f"Comparing is_error: Expected None, Got {block.is_error}") assert block.is_error is None # ================================================================================================== # Tests for AnthropicTool # ================================================================================================== class TestAnthropicTool: """Tests for AnthropicTool Pydantic model.""" def test_valid_tool(self): """ What it does: Verifies creation of valid AnthropicTool. Purpose: Ensure model accepts valid tool definition. """ print("Setup: Creating AnthropicTool with valid data...") tool = AnthropicTool( name="get_weather", description="Get weather for a location", input_schema={ "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } ) print(f"Result: {tool}") print(f"Comparing name: Expected 'get_weather', Got '{tool.name}'") assert tool.name == "get_weather" print(f"Comparing description: Got '{tool.description}'") assert tool.description == "Get weather for a location" print(f"Comparing input_schema: Got {tool.input_schema}") assert "properties" in tool.input_schema def test_requires_name(self): """ What it does: Verifies that name is required. Purpose: Ensure validation fails without name. """ print("Setup: Attempting to create AnthropicTool without name...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: AnthropicTool(input_schema={}) print(f"ValidationError raised: {exc_info.value}") assert "name" in str(exc_info.value) def test_requires_input_schema(self): """ What it does: Verifies that input_schema is required. Purpose: Ensure validation fails without input_schema. """ print("Setup: Attempting to create AnthropicTool without input_schema...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: AnthropicTool(name="test") print(f"ValidationError raised: {exc_info.value}") assert "input_schema" in str(exc_info.value) def test_description_is_optional(self): """ What it does: Verifies that description is optional. Purpose: Ensure tools without description work. """ print("Setup: Creating AnthropicTool without description...") tool = AnthropicTool(name="simple_tool", input_schema={}) print(f"Comparing description: Expected None, Got {tool.description}") assert tool.description is None # ================================================================================================== # Tests for ToolChoice models # ================================================================================================== class TestToolChoiceModels: """Tests for ToolChoice Pydantic models.""" def test_tool_choice_auto(self): """ What it does: Verifies creation of ToolChoiceAuto. Purpose: Ensure auto tool choice works. """ print("Setup: Creating ToolChoiceAuto...") choice = ToolChoiceAuto() print(f"Result: {choice}") print(f"Comparing type: Expected 'auto', Got '{choice.type}'") assert choice.type == "auto" def test_tool_choice_any(self): """ What it does: Verifies creation of ToolChoiceAny. Purpose: Ensure any tool choice works. """ print("Setup: Creating ToolChoiceAny...") choice = ToolChoiceAny() print(f"Result: {choice}") print(f"Comparing type: Expected 'any', Got '{choice.type}'") assert choice.type == "any" def test_tool_choice_tool(self): """ What it does: Verifies creation of ToolChoiceTool. Purpose: Ensure specific tool choice works. """ print("Setup: Creating ToolChoiceTool...") choice = ToolChoiceTool(name="get_weather") print(f"Result: {choice}") print(f"Comparing type: Expected 'tool', Got '{choice.type}'") assert choice.type == "tool" print(f"Comparing name: Expected 'get_weather', Got '{choice.name}'") assert choice.name == "get_weather" def test_tool_choice_tool_requires_name(self): """ What it does: Verifies that ToolChoiceTool requires name. Purpose: Ensure validation fails without name. """ print("Setup: Attempting to create ToolChoiceTool without name...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: ToolChoiceTool() print(f"ValidationError raised: {exc_info.value}") assert "name" in str(exc_info.value) # ================================================================================================== # Tests for SystemContentBlock # ================================================================================================== class TestSystemContentBlock: """Tests for SystemContentBlock Pydantic model.""" def test_valid_system_block(self): """ What it does: Verifies creation of valid SystemContentBlock. Purpose: Ensure model accepts valid system content. """ print("Setup: Creating SystemContentBlock with valid data...") block = SystemContentBlock(text="You are a helpful assistant.") print(f"Result: {block}") print(f"Comparing type: Expected 'text', Got '{block.type}'") assert block.type == "text" print(f"Comparing text: Got '{block.text}'") assert block.text == "You are a helpful assistant." def test_with_cache_control(self): """ What it does: Verifies SystemContentBlock with cache_control. Purpose: Ensure prompt caching format works. """ print("Setup: Creating SystemContentBlock with cache_control...") block = SystemContentBlock( text="You are helpful.", cache_control={"type": "ephemeral"} ) print(f"Result: {block}") print(f"Comparing cache_control: Got {block.cache_control}") assert block.cache_control == {"type": "ephemeral"} def test_cache_control_is_optional(self): """ What it does: Verifies that cache_control is optional. Purpose: Ensure blocks without cache_control work. """ print("Setup: Creating SystemContentBlock without cache_control...") block = SystemContentBlock(text="Test") print(f"Comparing cache_control: Expected None, Got {block.cache_control}") assert block.cache_control is None def test_requires_text(self): """ What it does: Verifies that text is required. Purpose: Ensure validation fails without text. """ print("Setup: Attempting to create SystemContentBlock without text...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: SystemContentBlock() print(f"ValidationError raised: {exc_info.value}") assert "text" in str(exc_info.value) # ================================================================================================== # Tests for AnthropicUsage # ================================================================================================== class TestAnthropicUsage: """Tests for AnthropicUsage Pydantic model.""" def test_valid_usage(self): """ What it does: Verifies creation of valid AnthropicUsage. Purpose: Ensure model accepts valid usage data. """ print("Setup: Creating AnthropicUsage with valid data...") usage = AnthropicUsage(input_tokens=100, output_tokens=50) print(f"Result: {usage}") print(f"Comparing input_tokens: Expected 100, Got {usage.input_tokens}") assert usage.input_tokens == 100 print(f"Comparing output_tokens: Expected 50, Got {usage.output_tokens}") assert usage.output_tokens == 50 def test_requires_input_tokens(self): """ What it does: Verifies that input_tokens is required. Purpose: Ensure validation fails without input_tokens. """ print("Setup: Attempting to create AnthropicUsage without input_tokens...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: AnthropicUsage(output_tokens=50) print(f"ValidationError raised: {exc_info.value}") assert "input_tokens" in str(exc_info.value) def test_requires_output_tokens(self): """ What it does: Verifies that output_tokens is required. Purpose: Ensure validation fails without output_tokens. """ print("Setup: Attempting to create AnthropicUsage without output_tokens...") print("Action: Creating model (should raise ValidationError)...") with pytest.raises(ValidationError) as exc_info: AnthropicUsage(input_tokens=100) print(f"ValidationError raised: {exc_info.value}") assert "output_tokens" in str(exc_info.value) # ================================================================================================== # Tests for AnthropicMessagesResponse # ================================================================================================== class TestAnthropicMessagesResponse: """Tests for AnthropicMessagesResponse Pydantic model.""" def test_valid_response(self): """ What it does: Verifies creation of valid AnthropicMessagesResponse. Purpose: Ensure model accepts valid response data. """ print("Setup: Creating AnthropicMessagesResponse with valid data...") response = AnthropicMessagesResponse( id="msg_123", model="claude-sonnet-4-5", content=[TextContentBlock(text="Hello!")], usage=AnthropicUsage(input_tokens=10, output_tokens=5) ) print(f"Result: {response}") print(f"Comparing id: Expected 'msg_123', Got '{response.id}'") assert response.id == "msg_123" print(f"Comparing type: Expected 'message', Got '{response.type}'") assert response.type == "message" print(f"Comparing role: Expected 'assistant', Got '{response.role}'") assert response.role == "assistant" print(f"Comparing model: Expected 'claude-sonnet-4-5', Got '{response.model}'") assert response.model == "claude-sonnet-4-5" def test_stop_reason_values(self): """ What it does: Verifies that stop_reason accepts valid values. Purpose: Ensure all stop reasons work. """ print("Setup: Testing various stop_reason values...") stop_reasons = ["end_turn", "max_tokens", "stop_sequence", "tool_use"] for reason in stop_reasons: print(f"Testing stop_reason: {reason}") response = AnthropicMessagesResponse( id="msg_1", model="claude-sonnet-4-5", content=[TextContentBlock(text="Test")], usage=AnthropicUsage(input_tokens=1, output_tokens=1), stop_reason=reason ) assert response.stop_reason == reason print("All stop_reason values accepted successfully") def test_stop_reason_is_optional(self): """ What it does: Verifies that stop_reason is optional. Purpose: Ensure responses without stop_reason work. """ print("Setup: Creating response without stop_reason...") response = AnthropicMessagesResponse( id="msg_1", model="claude-sonnet-4-5", content=[TextContentBlock(text="Test")], usage=AnthropicUsage(input_tokens=1, output_tokens=1) ) print(f"Comparing stop_reason: Expected None, Got {response.stop_reason}") assert response.stop_reason is None # ================================================================================================== # Tests for Streaming Event Models # ================================================================================================== class TestStreamingEvents: """Tests for streaming event Pydantic models.""" def test_message_start_event(self): """ What it does: Verifies creation of MessageStartEvent. Purpose: Ensure message_start event works. """ print("Setup: Creating MessageStartEvent...") event = MessageStartEvent( message={"id": "msg_1", "type": "message", "role": "assistant"} ) print(f"Result: {event}") print(f"Comparing type: Expected 'message_start', Got '{event.type}'") assert event.type == "message_start" assert event.message["id"] == "msg_1" def test_content_block_start_event(self): """ What it does: Verifies creation of ContentBlockStartEvent. Purpose: Ensure content_block_start event works. """ print("Setup: Creating ContentBlockStartEvent...") event = ContentBlockStartEvent( index=0, content_block={"type": "text", "text": ""} ) print(f"Result: {event}") print(f"Comparing type: Expected 'content_block_start', Got '{event.type}'") assert event.type == "content_block_start" assert event.index == 0 def test_text_delta(self): """ What it does: Verifies creation of TextDelta. Purpose: Ensure text_delta works. """ print("Setup: Creating TextDelta...") delta = TextDelta(text="Hello") print(f"Result: {delta}") print(f"Comparing type: Expected 'text_delta', Got '{delta.type}'") assert delta.type == "text_delta" assert delta.text == "Hello" def test_thinking_delta(self): """ What it does: Verifies creation of ThinkingDelta. Purpose: Ensure thinking_delta works. """ print("Setup: Creating ThinkingDelta...") delta = ThinkingDelta(thinking="Let me think...") print(f"Result: {delta}") print(f"Comparing type: Expected 'thinking_delta', Got '{delta.type}'") assert delta.type == "thinking_delta" assert delta.thinking == "Let me think..." def test_input_json_delta(self): """ What it does: Verifies creation of InputJsonDelta. Purpose: Ensure input_json_delta works. """ print("Setup: Creating InputJsonDelta...") delta = InputJsonDelta(partial_json='{"loc') print(f"Result: {delta}") print(f"Comparing type: Expected 'input_json_delta', Got '{delta.type}'") assert delta.type == "input_json_delta" assert delta.partial_json == '{"loc' def test_content_block_delta_event(self): """ What it does: Verifies creation of ContentBlockDeltaEvent. Purpose: Ensure content_block_delta event works. """ print("Setup: Creating ContentBlockDeltaEvent...") event = ContentBlockDeltaEvent( index=0, delta=TextDelta(text="Hello") ) print(f"Result: {event}") print(f"Comparing type: Expected 'content_block_delta', Got '{event.type}'") assert event.type == "content_block_delta" assert event.index == 0 def test_content_block_stop_event(self): """ What it does: Verifies creation of ContentBlockStopEvent. Purpose: Ensure content_block_stop event works. """ print("Setup: Creating ContentBlockStopEvent...") event = ContentBlockStopEvent(index=0) print(f"Result: {event}") print(f"Comparing type: Expected 'content_block_stop', Got '{event.type}'") assert event.type == "content_block_stop" assert event.index == 0 def test_message_delta_event(self): """ What it does: Verifies creation of MessageDeltaEvent. Purpose: Ensure message_delta event works. """ print("Setup: Creating MessageDeltaEvent...") event = MessageDeltaEvent( delta={"stop_reason": "end_turn"}, usage=MessageDeltaUsage(output_tokens=10) ) print(f"Result: {event}") print(f"Comparing type: Expected 'message_delta', Got '{event.type}'") assert event.type == "message_delta" assert event.delta["stop_reason"] == "end_turn" def test_message_stop_event(self): """ What it does: Verifies creation of MessageStopEvent. Purpose: Ensure message_stop event works. """ print("Setup: Creating MessageStopEvent...") event = MessageStopEvent() print(f"Result: {event}") print(f"Comparing type: Expected 'message_stop', Got '{event.type}'") assert event.type == "message_stop" def test_ping_event(self): """ What it does: Verifies creation of PingEvent. Purpose: Ensure ping event works. """ print("Setup: Creating PingEvent...") event = PingEvent() print(f"Result: {event}") print(f"Comparing type: Expected 'ping', Got '{event.type}'") assert event.type == "ping" def test_error_event(self): """ What it does: Verifies creation of ErrorEvent. Purpose: Ensure error event works. """ print("Setup: Creating ErrorEvent...") event = ErrorEvent(error={"type": "invalid_request", "message": "Bad request"}) print(f"Result: {event}") print(f"Comparing type: Expected 'error', Got '{event.type}'") assert event.type == "error" assert event.error["type"] == "invalid_request" # ================================================================================================== # Tests for Error Models # ================================================================================================== class TestErrorModels: """Tests for error Pydantic models.""" def test_anthropic_error_detail(self): """ What it does: Verifies creation of AnthropicErrorDetail. Purpose: Ensure error detail model works. """ print("Setup: Creating AnthropicErrorDetail...") detail = AnthropicErrorDetail( type="invalid_request_error", message="Invalid API key" ) print(f"Result: {detail}") print(f"Comparing type: Expected 'invalid_request_error', Got '{detail.type}'") assert detail.type == "invalid_request_error" print(f"Comparing message: Got '{detail.message}'") assert detail.message == "Invalid API key" def test_anthropic_error_response(self): """ What it does: Verifies creation of AnthropicErrorResponse. Purpose: Ensure error response model works. """ print("Setup: Creating AnthropicErrorResponse...") response = AnthropicErrorResponse( error=AnthropicErrorDetail( type="authentication_error", message="Invalid API key provided" ) ) print(f"Result: {response}") print(f"Comparing type: Expected 'error', Got '{response.type}'") assert response.type == "error" print(f"Comparing error.type: Got '{response.error.type}'") assert response.error.type == "authentication_error"