diff --git a/.github/workflows/python-check-coverage.py b/.github/workflows/python-check-coverage.py index 9f48cbdcbf..0e6811ccd0 100644 --- a/.github/workflows/python-check-coverage.py +++ b/.github/workflows/python-check-coverage.py @@ -33,9 +33,9 @@ "packages.core.agent_framework", "packages.core.agent_framework._workflows", "packages.purview.agent_framework_purview", + "packages.anthropic.agent_framework_anthropic", # Add more modules here as coverage improves: # "packages.azure-ai-search.agent_framework_azure_ai_search", - # "packages.anthropic.agent_framework_anthropic", } @@ -62,7 +62,9 @@ def branch_coverage_percent(self) -> float: return self.branch_rate * 100 -def parse_coverage_xml(xml_path: str) -> tuple[dict[str, PackageCoverage], float, float]: +def parse_coverage_xml( + xml_path: str, +) -> tuple[dict[str, PackageCoverage], float, float]: """Parse Cobertura XML and extract per-package coverage data. Args: @@ -103,7 +105,9 @@ def parse_coverage_xml(xml_path: str) -> tuple[dict[str, PackageCoverage], float if condition_coverage: # Parse "X% (covered/total)" format try: - coverage_parts = condition_coverage.split("(")[1].rstrip(")").split("/") + coverage_parts = ( + condition_coverage.split("(")[1].rstrip(")").split("/") + ) branches_covered += int(coverage_parts[0]) branches_valid += int(coverage_parts[1]) except (IndexError, ValueError): @@ -114,7 +118,9 @@ def parse_coverage_xml(xml_path: str) -> tuple[dict[str, PackageCoverage], float packages[package_path] = PackageCoverage( name=package_path, line_rate=line_rate if lines_valid == 0 else lines_covered / lines_valid, - branch_rate=branch_rate if branches_valid == 0 else branches_covered / branches_valid, + branch_rate=branch_rate + if branches_valid == 0 + else branches_covered / branches_valid, lines_valid=lines_valid, lines_covered=lines_covered, branches_valid=branches_valid, @@ -179,7 +185,9 @@ def print_coverage_table( for pkg in sorted_packages: is_enforced = pkg.name in ENFORCED_MODULES enforced_marker = "[ENFORCED] " if is_enforced else "" - line_cov = format_coverage_value(pkg.line_coverage_percent, threshold, is_enforced) + line_cov = format_coverage_value( + pkg.line_coverage_percent, threshold, is_enforced + ) lines_info = f"{pkg.lines_covered}/{pkg.lines_valid}" package_label = f"{enforced_marker}{pkg.name}" @@ -217,11 +225,15 @@ def check_coverage(xml_path: str, threshold: float) -> bool: # Report results if missing_modules: - print(f"\n❌ FAILED: Enforced modules not found in coverage report: {', '.join(missing_modules)}") + print( + f"\n❌ FAILED: Enforced modules not found in coverage report: {', '.join(missing_modules)}" + ) return False if failed_modules: - print(f"\n❌ FAILED: The following enforced modules are below {threshold}% coverage threshold:") + print( + f"\n❌ FAILED: The following enforced modules are below {threshold}% coverage threshold:" + ) for module in failed_modules: print(f" - {module}") print("\nTo fix: Add more tests to improve coverage for the failing modules.") @@ -230,7 +242,9 @@ def check_coverage(xml_path: str, threshold: float) -> bool: if ENFORCED_MODULES: found_enforced = [m for m in ENFORCED_MODULES if m in packages] if found_enforced: - print(f"\n✅ PASSED: All enforced modules meet the {threshold}% coverage threshold.") + print( + f"\n✅ PASSED: All enforced modules meet the {threshold}% coverage threshold." + ) return True diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index ff9234f60b..fa18bfbffd 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -21,11 +21,14 @@ BetaToolUseBlock, BetaUsage, ) -from pydantic import Field +from pydantic import BaseModel, Field from agent_framework_anthropic import AnthropicClient from agent_framework_anthropic._chat_client import AnthropicSettings +# Test constants +VALID_PNG_BASE64 = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + skip_if_anthropic_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" or os.getenv("ANTHROPIC_API_KEY", "") in ("", "test-api-key-12345"), @@ -895,3 +898,944 @@ async def test_anthropic_client_integration_images() -> None: assert response is not None assert response.messages[0].text is not None assert "house" in response.messages[0].text.lower() + + +# Response Format Tests + + +def test_prepare_response_format_openai_style(mock_anthropic_client: MagicMock) -> None: + """Test response_format with OpenAI-style json_schema.""" + client = create_test_anthropic_client(mock_anthropic_client) + + response_format = { + "json_schema": { + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + } + } + } + + result = client._prepare_response_format(response_format) + + assert result["type"] == "json_schema" + assert result["schema"]["additionalProperties"] is False + assert result["schema"]["properties"]["name"]["type"] == "string" + + +def test_prepare_response_format_direct_schema(mock_anthropic_client: MagicMock) -> None: + """Test response_format with direct schema key.""" + client = create_test_anthropic_client(mock_anthropic_client) + + response_format = { + "schema": { + "type": "object", + "properties": {"value": {"type": "number"}}, + } + } + + result = client._prepare_response_format(response_format) + + assert result["type"] == "json_schema" + assert result["schema"]["additionalProperties"] is False + assert result["schema"]["properties"]["value"]["type"] == "number" + + +def test_prepare_response_format_raw_schema(mock_anthropic_client: MagicMock) -> None: + """Test response_format with raw schema dict.""" + client = create_test_anthropic_client(mock_anthropic_client) + + response_format = { + "type": "object", + "properties": {"count": {"type": "integer"}}, + } + + result = client._prepare_response_format(response_format) + + assert result["type"] == "json_schema" + assert result["schema"]["additionalProperties"] is False + assert result["schema"]["properties"]["count"]["type"] == "integer" + + +def test_prepare_response_format_pydantic_model(mock_anthropic_client: MagicMock) -> None: + """Test response_format with Pydantic BaseModel.""" + client = create_test_anthropic_client(mock_anthropic_client) + + class TestModel(BaseModel): + name: str + age: int + + result = client._prepare_response_format(TestModel) + + assert result["type"] == "json_schema" + assert result["schema"]["additionalProperties"] is False + assert "properties" in result["schema"] + + +# Message Preparation Tests + + +def test_prepare_message_with_image_data(mock_anthropic_client: MagicMock) -> None: + """Test preparing messages with base64-encoded image data.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create message with image data content + message = Message( + role="user", + contents=[Content.from_data(media_type="image/png", data=VALID_PNG_BASE64)], + ) + + result = client._prepare_message_for_anthropic(message) + + assert result["role"] == "user" + assert len(result["content"]) == 1 + assert result["content"][0]["type"] == "image" + assert result["content"][0]["source"]["type"] == "base64" + assert result["content"][0]["source"]["media_type"] == "image/png" + + +def test_prepare_message_with_image_uri(mock_anthropic_client: MagicMock) -> None: + """Test preparing messages with image URI.""" + client = create_test_anthropic_client(mock_anthropic_client) + + message = Message( + role="user", + contents=[Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")], + ) + + result = client._prepare_message_for_anthropic(message) + + assert result["role"] == "user" + assert len(result["content"]) == 1 + assert result["content"][0]["type"] == "image" + assert result["content"][0]["source"]["type"] == "url" + assert result["content"][0]["source"]["url"] == "https://example.com/image.jpg" + + +def test_prepare_message_with_unsupported_data_type( + mock_anthropic_client: MagicMock, +) -> None: + """Test preparing messages with unsupported data content type.""" + client = create_test_anthropic_client(mock_anthropic_client) + + message = Message( + role="user", + contents=[Content.from_data(media_type="application/pdf", data=b"PDF data")], + ) + + result = client._prepare_message_for_anthropic(message) + + # PDF should be ignored + assert result["role"] == "user" + assert len(result["content"]) == 0 + + +def test_prepare_message_with_unsupported_uri_type(mock_anthropic_client: MagicMock) -> None: + """Test preparing messages with unsupported URI content type.""" + client = create_test_anthropic_client(mock_anthropic_client) + + message = Message( + role="user", + contents=[Content.from_uri(uri="https://example.com/video.mp4", media_type="video/mp4")], + ) + + result = client._prepare_message_for_anthropic(message) + + # Video should be ignored + assert result["role"] == "user" + assert len(result["content"]) == 0 + + +# Content Parsing Tests + + +def test_parse_contents_mcp_tool_use(mock_anthropic_client: MagicMock) -> None: + """Test parsing MCP tool use content.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock MCP tool use block + mock_block = MagicMock() + mock_block.type = "mcp_tool_use" + mock_block.id = "call_123" + mock_block.name = "test_tool" + mock_block.input = {"arg": "value"} + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "mcp_server_tool_call" + + +def test_parse_contents_code_execution_tool(mock_anthropic_client: MagicMock) -> None: + """Test parsing code execution tool use.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock code execution tool use block + mock_block = MagicMock() + mock_block.type = "tool_use" + mock_block.id = "call_456" + mock_block.name = "code_execution_tool" + mock_block.input = "print('hello')" + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "code_interpreter_tool_call" + + +def test_parse_contents_mcp_tool_result_list_content( + mock_anthropic_client: MagicMock, +) -> None: + """Test parsing MCP tool result with list content.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_123", "test_tool") + + # Create mock MCP tool result with list content + mock_text_block = MagicMock() + mock_text_block.type = "text" + mock_text_block.text = "Result text" + + mock_block = MagicMock() + mock_block.type = "mcp_tool_result" + mock_block.tool_use_id = "call_123" + mock_block.content = [mock_text_block] + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "mcp_server_tool_result" + + +def test_parse_contents_mcp_tool_result_string_content( + mock_anthropic_client: MagicMock, +) -> None: + """Test parsing MCP tool result with string content.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_123", "test_tool") + + # Create mock MCP tool result with string content + mock_block = MagicMock() + mock_block.type = "mcp_tool_result" + mock_block.tool_use_id = "call_123" + mock_block.content = "Simple string result" + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "mcp_server_tool_result" + + +def test_parse_contents_mcp_tool_result_bytes_content( + mock_anthropic_client: MagicMock, +) -> None: + """Test parsing MCP tool result with bytes content.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_123", "test_tool") + + # Create mock MCP tool result with bytes content + mock_block = MagicMock() + mock_block.type = "mcp_tool_result" + mock_block.tool_use_id = "call_123" + mock_block.content = b"Binary data" + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "mcp_server_tool_result" + + +def test_parse_contents_mcp_tool_result_object_content( + mock_anthropic_client: MagicMock, +) -> None: + """Test parsing MCP tool result with object content.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_123", "test_tool") + + # Create mock MCP tool result with object content + mock_content_obj = MagicMock() + mock_content_obj.type = "text" + mock_content_obj.text = "Object content" + + mock_block = MagicMock() + mock_block.type = "mcp_tool_result" + mock_block.tool_use_id = "call_123" + mock_block.content = mock_content_obj + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "mcp_server_tool_result" + + +def test_parse_contents_web_search_tool_result(mock_anthropic_client: MagicMock) -> None: + """Test parsing web search tool result.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_789", "web_search") + + # Create mock web search tool result + mock_block = MagicMock() + mock_block.type = "web_search_tool_result" + mock_block.tool_use_id = "call_789" + mock_block.content = "Search results" + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +def test_parse_contents_web_fetch_tool_result(mock_anthropic_client: MagicMock) -> None: + """Test parsing web fetch tool result.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_101", "web_fetch") + + # Create mock web fetch tool result + mock_block = MagicMock() + mock_block.type = "web_fetch_tool_result" + mock_block.tool_use_id = "call_101" + mock_block.content = "Fetched content" + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +# MCP Tool Configuration Tests + + +def test_get_mcp_tool_with_allowed_tools() -> None: + """Test get_mcp_tool with allowed_tools parameter.""" + result = AnthropicClient.get_mcp_tool( + name="Test Server", + url="https://example.com/mcp", + allowed_tools=["tool1", "tool2"], + ) + + assert result["type"] == "mcp" + assert result["server_label"] == "Test_Server" + assert result["server_url"] == "https://example.com/mcp" + assert result["allowed_tools"] == ["tool1", "tool2"] + + +def test_get_mcp_tool_without_allowed_tools() -> None: + """Test get_mcp_tool without allowed_tools parameter.""" + result = AnthropicClient.get_mcp_tool(name="Test Server", url="https://example.com/mcp") + + assert result["type"] == "mcp" + assert result["server_label"] == "Test_Server" + assert result["server_url"] == "https://example.com/mcp" + assert "allowed_tools" not in result + + +def test_prepare_tools_mcp_with_allowed_tools(mock_anthropic_client: MagicMock) -> None: + """Test MCP tool with allowed_tools configuration.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + mcp_tool = { + "type": "mcp", + "server_label": "test_server", + "server_url": "https://example.com/mcp", + "allowed_tools": ["tool1", "tool2"], + } + + options = {"tools": [mcp_tool]} + + result = client._prepare_options(messages, options) + + assert "mcp_servers" in result + assert len(result["mcp_servers"]) == 1 + assert result["mcp_servers"][0]["tool_configuration"]["allowed_tools"] == [ + "tool1", + "tool2", + ] + + +# Tool Choice Mode Tests + + +def test_tool_choice_auto_with_allow_multiple(mock_anthropic_client: MagicMock) -> None: + """Test tool_choice auto mode with allow_multiple=False.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + @tool(approval_mode="never_require") + def test_func() -> str: + """Test function.""" + return "test" + + options = { + "tools": [test_func], + "tool_choice": "auto", + "allow_multiple_tool_calls": False, + } + + result = client._prepare_options(messages, options) + + assert result["tool_choice"]["type"] == "auto" + assert result["tool_choice"]["disable_parallel_tool_use"] is True + + +def test_tool_choice_required_any(mock_anthropic_client: MagicMock) -> None: + """Test tool_choice required mode without specific function.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + @tool(approval_mode="never_require") + def test_func() -> str: + """Test function.""" + return "test" + + options = {"tools": [test_func], "tool_choice": "required"} + + result = client._prepare_options(messages, options) + + assert result["tool_choice"]["type"] == "any" + + +def test_tool_choice_required_specific_function(mock_anthropic_client: MagicMock) -> None: + """Test tool_choice required mode with specific function.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + @tool(approval_mode="never_require") + def test_func() -> str: + """Test function.""" + return "test" + + options = { + "tools": [test_func], + "tool_choice": {"mode": "required", "required_function_name": "test_func"}, + } + + result = client._prepare_options(messages, options) + + assert result["tool_choice"]["type"] == "tool" + assert result["tool_choice"]["name"] == "test_func" + + +def test_tool_choice_none(mock_anthropic_client: MagicMock) -> None: + """Test tool_choice none mode.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + @tool(approval_mode="never_require") + def test_func() -> str: + """Test function.""" + return "test" + + options = {"tools": [test_func], "tool_choice": "none"} + + result = client._prepare_options(messages, options) + + assert result["tool_choice"]["type"] == "none" + + +def test_tool_choice_required_allows_parallel_use(mock_anthropic_client: MagicMock) -> None: + """Test tool choice required mode with allow_multiple=True.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + + @tool(approval_mode="never_require") + def test_func() -> str: + """Test function.""" + return "test" + + options = { + "tools": [test_func], + "tool_choice": "required", + "allow_multiple_tool_calls": True, + } + + # This tests line 739: setting disable_parallel_tool_use in required mode + result = client._prepare_options(messages, options) + + assert result["tool_choice"]["type"] == "any" + assert result["tool_choice"]["disable_parallel_tool_use"] is False + + +# Options Preparation Tests + + +def test_prepare_options_with_instructions(mock_anthropic_client: MagicMock) -> None: + """Test prepare_options with instructions parameter.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + options = {"instructions": "You are a helpful assistant"} + + result = client._prepare_options(messages, options) + + # Instructions should be prepended as system message + assert result["model"] == "claude-3-5-sonnet-20241022" + assert result["max_tokens"] == 1024 + + +def test_prepare_options_missing_model_id(mock_anthropic_client: MagicMock) -> None: + """Test prepare_options raises error when model_id is missing.""" + client = create_test_anthropic_client(mock_anthropic_client) + client.model_id = "" # Set empty model_id + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + options = {} + + try: + client._prepare_options(messages, options) + raise AssertionError("Expected ValueError") + except ValueError as e: + assert "model_id must be a non-empty string" in str(e) + + +def test_prepare_options_with_user_metadata(mock_anthropic_client: MagicMock) -> None: + """Test prepare_options maps user to metadata.user_id.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + options = {"user": "user123"} + + result = client._prepare_options(messages, options) + + assert "user" not in result + assert result["metadata"]["user_id"] == "user123" + + +def test_prepare_options_user_metadata_no_override( + mock_anthropic_client: MagicMock, +) -> None: + """Test user option doesn't override existing user_id in metadata.""" + client = create_test_anthropic_client(mock_anthropic_client) + + messages = [Message(role="user", contents=[Content.from_text("Hello")])] + options = {"user": "user123", "metadata": {"user_id": "existing_user"}} + + result = client._prepare_options(messages, options) + + # Existing user_id should be preserved + assert result["metadata"]["user_id"] == "existing_user" + + +def test_process_stream_event_message_stop(mock_anthropic_client: MagicMock) -> None: + """Test processing message_stop event.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # message_stop events don't produce output + mock_event = MagicMock() + mock_event.type = "message_stop" + + result = client._process_stream_event(mock_event) + + assert result is None + + +def test_parse_usage_with_cache_tokens(mock_anthropic_client: MagicMock) -> None: + """Test parsing usage with cache creation and read tokens.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock usage with cache tokens + mock_usage = MagicMock() + mock_usage.input_tokens = 100 + mock_usage.output_tokens = 50 + mock_usage.cache_creation_input_tokens = 20 + mock_usage.cache_read_input_tokens = 30 + + result = client._parse_usage_from_anthropic(mock_usage) + + assert result is not None + assert result["output_token_count"] == 50 + assert result["input_token_count"] == 100 + assert result["anthropic.cache_creation_input_tokens"] == 20 + assert result["anthropic.cache_read_input_tokens"] == 30 + + +# Code Execution Result Tests + + +def test_parse_code_execution_result_with_error(mock_anthropic_client: MagicMock) -> None: + """Test parsing code execution result with error.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_code1", "code_execution_tool") + + # Create mock code execution result with error + from anthropic.types.beta.beta_code_execution_tool_result_error import ( + BetaCodeExecutionToolResultError, + ) + + mock_block = MagicMock() + mock_block.type = "code_execution_tool_result" + mock_block.tool_use_id = "call_code1" + mock_block.content = BetaCodeExecutionToolResultError( + type="code_execution_tool_result_error", error_code="execution_time_exceeded" + ) + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "code_interpreter_tool_result" + + +def test_parse_code_execution_result_with_stdout(mock_anthropic_client: MagicMock) -> None: + """Test parsing code execution result with stdout.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_code2", "code_execution_tool") + + # Create mock code execution result with stdout + mock_content = MagicMock() + mock_content.stdout = "Hello, world!" + mock_content.stderr = None + mock_content.content = [] + + mock_block = MagicMock() + mock_block.type = "code_execution_tool_result" + mock_block.tool_use_id = "call_code2" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "code_interpreter_tool_result" + + +def test_parse_code_execution_result_with_stderr(mock_anthropic_client: MagicMock) -> None: + """Test parsing code execution result with stderr.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_code3", "code_execution_tool") + + # Create mock code execution result with stderr + mock_content = MagicMock() + mock_content.stdout = None + mock_content.stderr = "Warning message" + mock_content.content = [] + + mock_block = MagicMock() + mock_block.type = "code_execution_tool_result" + mock_block.tool_use_id = "call_code3" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "code_interpreter_tool_result" + + +def test_parse_code_execution_result_with_files(mock_anthropic_client: MagicMock) -> None: + """Test parsing code execution result with file outputs.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_code4", "code_execution_tool") + + # Create mock file output + mock_file = MagicMock() + mock_file.file_id = "file_123" + + # Create mock code execution result with files + mock_content = MagicMock() + mock_content.stdout = None + mock_content.stderr = None + mock_content.content = [mock_file] + + mock_block = MagicMock() + mock_block.type = "code_execution_tool_result" + mock_block.tool_use_id = "call_code4" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "code_interpreter_tool_result" + + +# Bash Execution Result Tests + + +def test_parse_bash_execution_result_with_stdout(mock_anthropic_client: MagicMock) -> None: + """Test parsing bash execution result with stdout.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_bash2", "bash_code_execution") + + # Create mock bash execution result with stdout + mock_content = MagicMock() + mock_content.stdout = "Output text" + mock_content.stderr = None + mock_content.content = [] + + mock_block = MagicMock() + mock_block.type = "bash_code_execution_tool_result" + mock_block.tool_use_id = "call_bash2" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +def test_parse_bash_execution_result_with_stderr(mock_anthropic_client: MagicMock) -> None: + """Test parsing bash execution result with stderr.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_bash3", "bash_code_execution") + + # Create mock bash execution result with stderr + mock_content = MagicMock() + mock_content.stdout = None + mock_content.stderr = "Error output" + mock_content.content = [] + + mock_block = MagicMock() + mock_block.type = "bash_code_execution_tool_result" + mock_block.tool_use_id = "call_bash3" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +# Text Editor Result Tests + + +def test_parse_text_editor_result_error(mock_anthropic_client: MagicMock) -> None: + """Test parsing text editor result with error.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_editor1", "text_editor_code_execution") + + # Create mock text editor result with error + mock_content = MagicMock() + mock_content.type = "text_editor_code_execution_tool_result_error" + mock_content.error = "File not found" + + mock_block = MagicMock() + mock_block.type = "text_editor_code_execution_tool_result" + mock_block.tool_use_id = "call_editor1" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +def test_parse_text_editor_result_view(mock_anthropic_client: MagicMock) -> None: + """Test parsing text editor view result.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_editor2", "text_editor_code_execution") + + # Create mock text editor view result + mock_content = MagicMock() + mock_content.type = "text_editor_code_execution_view_result" + mock_content.content = "File content" + mock_content.start_line = 10 + mock_content.num_lines = 5 + + mock_block = MagicMock() + mock_block.type = "text_editor_code_execution_tool_result" + mock_block.tool_use_id = "call_editor2" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +def test_parse_text_editor_result_str_replace(mock_anthropic_client: MagicMock) -> None: + """Test parsing text editor string replace result.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_editor3", "text_editor_code_execution") + + # Create mock text editor str_replace result + mock_content = MagicMock() + mock_content.type = "text_editor_code_execution_str_replace_result" + mock_content.old_start = 5 + mock_content.old_lines = 3 + mock_content.new_start = 5 + mock_content.new_lines = 4 + mock_content.lines = ["line1", "line2", "line3", "line4"] + + mock_block = MagicMock() + mock_block.type = "text_editor_code_execution_tool_result" + mock_block.tool_use_id = "call_editor3" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +def test_parse_text_editor_result_file_create(mock_anthropic_client: MagicMock) -> None: + """Test parsing text editor file create result.""" + client = create_test_anthropic_client(mock_anthropic_client) + client._last_call_id_name = ("call_editor4", "text_editor_code_execution") + + # Create mock text editor create result + mock_content = MagicMock() + mock_content.type = "text_editor_code_execution_create_result" + mock_content.is_file_update = False + + mock_block = MagicMock() + mock_block.type = "text_editor_code_execution_tool_result" + mock_block.tool_use_id = "call_editor4" + mock_block.content = mock_content + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "function_result" + + +# Thinking Block Tests + + +def test_parse_thinking_block(mock_anthropic_client: MagicMock) -> None: + """Test parsing thinking content block.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock thinking block + mock_block = MagicMock() + mock_block.type = "thinking" + mock_block.thinking = "Let me think about this..." + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "text_reasoning" + + +def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None: + """Test parsing thinking delta content block.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock thinking delta block + mock_block = MagicMock() + mock_block.type = "thinking_delta" + mock_block.thinking = "more thinking..." + + result = client._parse_contents_from_anthropic([mock_block]) + + assert len(result) == 1 + assert result[0].type == "text_reasoning" + + +# Citation Tests + + +def test_parse_citations_char_location(mock_anthropic_client: MagicMock) -> None: + """Test parsing citations with char_location.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock text block with citations + mock_citation = MagicMock() + mock_citation.type = "char_location" + mock_citation.title = "Source Title" + mock_citation.cited_text = "Citation snippet" + mock_citation.start_char_index = 0 + mock_citation.end_char_index = 10 + mock_citation.file_id = None + + mock_block = MagicMock() + mock_block.type = "text" + mock_block.text = "Text with citation" + mock_block.citations = [mock_citation] + + result = client._parse_citations_from_anthropic(mock_block) + + assert len(result) > 0 + + +def test_parse_citations_page_location(mock_anthropic_client: MagicMock) -> None: + """Test parsing citations with page_location.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock citation with page location + mock_citation = MagicMock() + mock_citation.type = "page_location" + mock_citation.document_title = "Document Title" + mock_citation.cited_text = "Cited text from page" + mock_citation.start_page_number = 1 + mock_citation.end_page_number = 3 + mock_citation.file_id = None + + mock_block = MagicMock() + mock_block.type = "text" + mock_block.text = "Text with page citation" + mock_block.citations = [mock_citation] + + result = client._parse_citations_from_anthropic(mock_block) + + assert len(result) > 0 + + +def test_parse_citations_content_block_location(mock_anthropic_client: MagicMock) -> None: + """Test parsing citations with content_block_location.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock citation with content block location + mock_citation = MagicMock() + mock_citation.type = "content_block_location" + mock_citation.document_title = "Document Title" + mock_citation.cited_text = "Cited text from content blocks" + mock_citation.start_block_index = 0 + mock_citation.end_block_index = 2 + mock_citation.file_id = None + + mock_block = MagicMock() + mock_block.type = "text" + mock_block.text = "Text with block citation" + mock_block.citations = [mock_citation] + + result = client._parse_citations_from_anthropic(mock_block) + + assert len(result) > 0 + + +def test_parse_citations_web_search_location(mock_anthropic_client: MagicMock) -> None: + """Test parsing citations with web_search_result_location.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock citation with web search location + mock_citation = MagicMock() + mock_citation.type = "web_search_result_location" + mock_citation.title = "Search Result" + mock_citation.cited_text = "Cited text from search" + mock_citation.url = "https://example.com" + mock_citation.file_id = None + + mock_block = MagicMock() + mock_block.type = "text" + mock_block.text = "Text with web citation" + mock_block.citations = [mock_citation] + + result = client._parse_citations_from_anthropic(mock_block) + + assert len(result) > 0 + + +def test_parse_citations_search_result_location(mock_anthropic_client: MagicMock) -> None: + """Test parsing citations with search_result_location.""" + client = create_test_anthropic_client(mock_anthropic_client) + + # Create mock citation with search result location + mock_citation = MagicMock() + mock_citation.type = "search_result_location" + mock_citation.title = "Search Result" + mock_citation.cited_text = "Cited text" + mock_citation.source = "https://source.com" + mock_citation.start_block_index = 0 + mock_citation.end_block_index = 1 + mock_citation.file_id = None + + mock_block = MagicMock() + mock_block.type = "text" + mock_block.text = "Text with search citation" + mock_block.citations = [mock_citation] + + result = client._parse_citations_from_anthropic(mock_block) + + assert len(result) > 0