diff --git a/tests/components/anthropic/snapshots/test_conversation.ambr b/tests/components/anthropic/snapshots/test_conversation.ambr index bfd402739427..3d168e1e345f 100644 --- a/tests/components/anthropic/snapshots/test_conversation.ambr +++ b/tests/components/anthropic/snapshots/test_conversation.ambr @@ -201,7 +201,7 @@ }), ]) # --- -# name: test_disabled_thinking[subentry_data0] +# name: test_disabled_thinking[zero_budget] list([ dict({ 'content': ''' @@ -230,7 +230,7 @@ }), ]) # --- -# name: test_disabled_thinking[subentry_data0].1 +# name: test_disabled_thinking[zero_budget].1 dict({ 'container': None, 'max_tokens': 3000, @@ -265,7 +265,7 @@ }), }) # --- -# name: test_disabled_thinking[subentry_data1] +# name: test_disabled_thinking[no_effort] list([ dict({ 'content': ''' @@ -294,7 +294,7 @@ }), ]) # --- -# name: test_disabled_thinking[subentry_data1].1 +# name: test_disabled_thinking[no_effort].1 dict({ 'container': None, 'max_tokens': 3000, @@ -513,7 +513,7 @@ }), ]) # --- -# name: test_history_conversion[content0] +# name: test_history_conversion[system_only] list([ dict({ 'content': 'Are you sure?', @@ -525,7 +525,7 @@ }), ]) # --- -# name: test_history_conversion[content1] +# name: test_history_conversion[single_exchange] list([ dict({ 'content': 'What shape is a donut?', @@ -545,7 +545,7 @@ }), ]) # --- -# name: test_history_conversion[content2] +# name: test_history_conversion[two_consecutive_messages] list([ dict({ 'content': list([ @@ -583,7 +583,7 @@ }), ]) # --- -# name: test_history_conversion[content3] +# name: test_history_conversion[three_consecutive_messages] list([ dict({ 'content': list([ @@ -629,7 +629,7 @@ }), ]) # --- -# name: test_history_conversion[content4] +# name: test_history_conversion[tool_results] list([ dict({ 'content': 'Turn off the lights and make me coffee', @@ -694,7 +694,7 @@ }), ]) # --- -# name: test_history_conversion[content5] +# name: test_history_conversion[web_search_citations] list([ dict({ 'content': "What's on the news today?", @@ -806,7 +806,7 @@ }), ]) # --- -# name: test_history_conversion[content6] +# name: test_history_conversion[web_fetch_citations] list([ dict({ 'content': "What's new in Home Assistant?", @@ -899,7 +899,7 @@ }), ]) # --- -# name: test_history_conversion[content7] +# name: test_history_conversion[time_in_tool_result] list([ dict({ 'content': 'What time is it?', @@ -946,7 +946,7 @@ }), ]) # --- -# name: test_history_conversion[content8] +# name: test_history_conversion[tool_search_results] list([ dict({ 'content': 'Set humidity to 50%', @@ -1088,7 +1088,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts0-content0] +# name: test_text_editor_code_execution[create_file] list([ dict({ 'attachments': None, @@ -1147,7 +1147,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts0-content0].1 +# name: test_text_editor_code_execution[create_file].1 list([ dict({ 'content': 'Do the needful', @@ -1186,7 +1186,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts1-content1] +# name: test_text_editor_code_execution[replace_text] list([ dict({ 'attachments': None, @@ -1255,7 +1255,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts1-content1].1 +# name: test_text_editor_code_execution[replace_text].1 list([ dict({ 'content': 'Do the needful', @@ -1304,7 +1304,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts2-content2] +# name: test_text_editor_code_execution[view_file] list([ dict({ 'attachments': None, @@ -1366,7 +1366,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts2-content2].1 +# name: test_text_editor_code_execution[view_file].1 list([ dict({ 'content': 'Do the needful', @@ -1408,7 +1408,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts3-content3] +# name: test_text_editor_code_execution[tool_error] list([ dict({ 'attachments': None, @@ -1467,7 +1467,7 @@ }), ]) # --- -# name: test_text_editor_code_execution[args_parts3-content3].1 +# name: test_text_editor_code_execution[tool_error].1 list([ dict({ 'content': 'Do the needful', diff --git a/tests/components/anthropic/test_ai_task.py b/tests/components/anthropic/test_ai_task.py index 10f7e8a82ce6..0ad3e7c2b67c 100644 --- a/tests/components/anthropic/test_ai_task.py +++ b/tests/components/anthropic/test_ai_task.py @@ -24,10 +24,10 @@ from . import create_content_block, create_thinking_block, create_tool_use_block from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_init_component") async def test_generate_data( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, entity_registry: er.EntityRegistry, ) -> None: @@ -59,10 +59,8 @@ async def test_generate_data( assert result.data == "The test data" +@pytest.mark.usefixtures("mock_init_component") async def test_translation_key( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, entity_registry: er.EntityRegistry, ) -> None: """Test entity translation key.""" @@ -71,10 +69,9 @@ async def test_translation_key( assert entry.translation_key == "ai_task_data" +@pytest.mark.usefixtures("mock_init_component") async def test_empty_data( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test AI Task data generation but the data returned is empty.""" @@ -91,10 +88,9 @@ async def test_empty_data( ) +@pytest.mark.usefixtures("mock_init_component") async def test_stream_wrong_type( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test error if the response is not a stream.""" @@ -117,10 +113,10 @@ async def test_stream_wrong_type( @freeze_time("2026-01-01 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_generate_structured_data_legacy( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -168,10 +164,10 @@ async def test_generate_structured_data_legacy( @freeze_time("2026-01-01 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_generate_structured_data_legacy_tools( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -220,10 +216,10 @@ async def test_generate_structured_data_legacy_tools( @freeze_time("2026-01-01 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_generate_structured_data_legacy_extended_thinking( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -280,10 +276,10 @@ async def test_generate_structured_data_legacy_extended_thinking( @freeze_time("2026-01-01 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_generate_structured_data_legacy_extra_text_block( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -340,10 +336,10 @@ async def test_generate_structured_data_legacy_extra_text_block( assert mock_create_stream.call_args.kwargs.copy() == snapshot +@pytest.mark.usefixtures("mock_init_component") async def test_generate_invalid_structured_data_legacy( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test AI Task with invalid JSON response with legacy method.""" @@ -389,10 +385,9 @@ async def test_generate_invalid_structured_data_legacy( @freeze_time("2026-01-01 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_generate_structured_data( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -423,12 +418,10 @@ async def test_generate_structured_data( assert mock_create_stream.call_args.kwargs.copy() == snapshot +@pytest.mark.usefixtures("mock_init_component") async def test_generate_data_with_attachments( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, - entity_registry: er.EntityRegistry, ) -> None: """Test AI Task data generation with attachments.""" entity_id = "ai_task.claude_ai_task" @@ -505,12 +498,10 @@ async def test_generate_data_with_attachments( assert document_block["source"]["type"] == "base64" +@pytest.mark.usefixtures("mock_init_component") async def test_generate_data_invalid_attachments( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, - entity_registry: er.EntityRegistry, ) -> None: """Test AI Task data generation with attachments of unsupported type.""" entity_id = "ai_task.claude_ai_task" @@ -581,10 +572,9 @@ async def test_generate_data_invalid_attachments( ) +@pytest.mark.usefixtures("mock_init_component") async def test_generate_data_with_attachments_whitespace_instructions( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test whitespace-only instructions with attachments produce no text block. diff --git a/tests/components/anthropic/test_config_flow.py b/tests/components/anthropic/test_config_flow.py index 606a42c734e4..99c4f1044c48 100644 --- a/tests/components/anthropic/test_config_flow.py +++ b/tests/components/anthropic/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from anthropic import ( APIConnectionError, + APIError, APIResponseValidationError, APITimeoutError, AuthenticationError, @@ -53,7 +54,10 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -94,7 +98,10 @@ async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_duplicate_entry(hass: HomeAssistant, mock_config_entry) -> None: +async def test_duplicate_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test we abort on duplicate config entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -117,8 +124,10 @@ async def test_duplicate_entry(hass: HomeAssistant, mock_config_entry) -> None: assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_init_component") async def test_creating_conversation_subentry( - hass: HomeAssistant, mock_config_entry, mock_init_component + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation subentry.""" result = await hass.config_entries.subentries.async_init( @@ -144,9 +153,9 @@ async def test_creating_conversation_subentry( assert result2["data"] == processed_options +@pytest.mark.usefixtures("mock_init_component") async def test_creating_conversation_subentry_not_loaded( hass: HomeAssistant, - mock_init_component, mock_config_entry: MockConfigEntry, ) -> None: """Test creating a conversation subentry when entry is not loaded.""" @@ -167,9 +176,11 @@ async def test_creating_conversation_subentry_not_loaded( @pytest.mark.parametrize( ("side_effect", "error"), [ - (APIConnectionError(request=None), "cannot_connect"), - (APITimeoutError(request=None), "timeout_connect"), - ( + pytest.param( + APIConnectionError(request=None), "cannot_connect", id="connection_error" + ), + pytest.param(APITimeoutError(request=None), "timeout_connect", id="timeout"), + pytest.param( BadRequestError( message=( "Your credit balance is too low to access" @@ -183,8 +194,9 @@ async def test_creating_conversation_subentry_not_loaded( body={"type": "error", "error": {"type": "invalid_request_error"}}, ), "unknown", + id="insufficient_credit", ), - ( + pytest.param( AuthenticationError( message="invalid x-api-key", response=Response( @@ -194,8 +206,9 @@ async def test_creating_conversation_subentry_not_loaded( body={"type": "error", "error": {"type": "authentication_error"}}, ), "authentication_error", + id="authentication_error", ), - ( + pytest.param( InternalServerError( message=None, response=Response( @@ -205,8 +218,9 @@ async def test_creating_conversation_subentry_not_loaded( body=None, ), "unknown", + id="server_error", ), - ( + pytest.param( APIResponseValidationError( response=Response( status_code=200, @@ -215,11 +229,16 @@ async def test_creating_conversation_subentry_not_loaded( body=None, ), "unknown", + id="invalid_response", ), ], ) @pytest.mark.usefixtures("mock_setup_entry") -async def test_api_error(hass: HomeAssistant, side_effect, error) -> None: +async def test_api_error( + hass: HomeAssistant, + side_effect: APIError, + error: str, +) -> None: """Test that we handle API errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -257,8 +276,10 @@ async def test_api_error(hass: HomeAssistant, side_effect, error) -> None: } +@pytest.mark.usefixtures("mock_init_component") async def test_subentry_options_thinking_budget_more_than_max( - hass: HomeAssistant, mock_config_entry, mock_init_component + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test error about thinking budget being more than max tokens.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -312,8 +333,10 @@ async def test_subentry_options_thinking_budget_more_than_max( assert subentry.data["thinking_budget"] == 8192 +@pytest.mark.usefixtures("mock_init_component") async def test_subentry_web_search_user_location( - hass: HomeAssistant, mock_config_entry, mock_init_component + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test fetching user location.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -403,10 +426,10 @@ async def test_subentry_web_search_user_location( } +@pytest.mark.usefixtures("mock_init_component") async def test_model_list( hass: HomeAssistant, - mock_config_entry, - mock_init_component, + mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test fetching and processing the list of models.""" @@ -428,8 +451,10 @@ async def test_model_list( assert options["data_schema"].schema["chat_model"].config["options"] == snapshot +@pytest.mark.usefixtures("mock_init_component") async def test_invalid_model( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component: None + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test exceptions during fetching model info.""" options = await hass.config_entries.subentries.async_init( @@ -534,7 +559,7 @@ async def test_invalid_model( @pytest.mark.parametrize( ("current_options", "new_options", "expected_options"), [ - ( # Test converting single llm api format to list + pytest.param( # Test converting single llm api format to list { CONF_RECOMMENDED: True, CONF_PROMPT: "", @@ -552,8 +577,9 @@ async def test_invalid_model( CONF_PROMPT: "", CONF_LLM_HASS_API: ["assist"], }, + id="recommended_to_recommended", ), - ( # Model with web search options + pytest.param( # Model with web search options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-sonnet-4-5", @@ -604,8 +630,9 @@ async def test_invalid_model( CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, + id="disable_web_tools", ), - ( # Model with thinking budget options + pytest.param( # Model with thinking budget options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-sonnet-4-5", @@ -656,8 +683,9 @@ async def test_invalid_model( CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, + id="update_thinking_budget", ), - ( # Model with thinking effort options + pytest.param( # Model with thinking effort options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-fable-5", @@ -709,8 +737,9 @@ async def test_invalid_model( CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: True, }, + id="update_thinking_effort", ), - ( # Test switching from recommended to custom options + pytest.param( # Test switching from recommended to custom options { CONF_RECOMMENDED: True, CONF_PROMPT: "bla", @@ -742,8 +771,9 @@ async def test_invalid_model( CONF_WEB_FETCH_MAX_USES: 5, CONF_CODE_EXECUTION: False, }, + id="recommended_to_custom", ), - ( # Test switching from custom to recommended options + pytest.param( # Test switching from custom to recommended options { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", @@ -771,16 +801,17 @@ async def test_invalid_model( CONF_LLM_HASS_API: ["assist"], CONF_PROMPT: "", }, + id="custom_to_recommended", ), ], ) +@pytest.mark.usefixtures("mock_init_component") async def test_subentry_options_switching( hass: HomeAssistant, - mock_config_entry, - mock_init_component, - current_options, - new_options, - expected_options, + mock_config_entry: MockConfigEntry, + current_options: dict[str, str | int | bool | list[str]], + new_options: tuple[dict[str, str | int | bool | list[str]], ...], + expected_options: dict[str, str | int | bool | list[str]], ) -> None: """Test the subentry options form.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -822,10 +853,10 @@ async def test_subentry_options_switching( assert subentry.data == expected_options +@pytest.mark.usefixtures("mock_init_component") async def test_creating_ai_task_subentry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, ) -> None: """Test creating an AI task subentry.""" old_subentries = set(mock_config_entry.subentries) @@ -880,10 +911,10 @@ async def test_ai_task_subentry_not_loaded( assert result.get("reason") == "entry_not_loaded" +@pytest.mark.usefixtures("mock_init_component") async def test_creating_ai_task_subentry_additional( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, ) -> None: """Test creating an AI task subentry with additional settings.""" result = await hass.config_entries.subentries.async_init( @@ -945,7 +976,9 @@ async def test_creating_ai_task_subentry_additional( @pytest.mark.usefixtures("mock_setup_entry") -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth( + hass: HomeAssistant, +) -> None: """Test we can reauthenticate.""" # Pretend we already set up a config entry. hass.config.components.add("anthropic") @@ -980,18 +1013,20 @@ async def test_reauth(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("current_llm_apis", "suggested_llm_apis", "expected_options"), [ - ("assist", ["assist"], ["assist"]), - (["assist"], ["assist"], ["assist"]), - ("non-existent", [], ["assist"]), - (["non-existent"], [], ["assist"]), - (["assist", "non-existent"], ["assist"], ["assist"]), + pytest.param("assist", ["assist"], ["assist"], id="assist_string"), + pytest.param(["assist"], ["assist"], ["assist"], id="assist_list"), + pytest.param("non-existent", [], ["assist"], id="unknown_string"), + pytest.param(["non-existent"], [], ["assist"], id="unknown_list"), + pytest.param( + ["assist", "non-existent"], ["assist"], ["assist"], id="mixed_list" + ), ], ) +@pytest.mark.usefixtures("mock_init_component") async def test_reconfigure_conversation_subentry_llm_api_schema( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, - current_llm_apis: list[str], + current_llm_apis: str | list[str], suggested_llm_apis: list[str], expected_options: list[str], ) -> None: diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 78dd9828b4b9..e0939740a143 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -101,8 +101,10 @@ from . import ( from tests.common import MockConfigEntry +@pytest.mark.usefixtures("mock_init_component") async def test_entity( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, ) -> None: """Test entity properties.""" state = hass.states.get("conversation.claude_conversation") @@ -129,11 +131,10 @@ async def test_entity( ) +@pytest.mark.usefixtures("mock_init_component") async def test_device( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry, - mock_init_component, ) -> None: """Test device parameters.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -149,10 +150,8 @@ async def test_device( assert device.entry_type == dr.DeviceEntryType.SERVICE +@pytest.mark.usefixtures("mock_init_component") async def test_translation_key( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, entity_registry: er.EntityRegistry, ) -> None: """Test entity translation key.""" @@ -161,10 +160,9 @@ async def test_translation_key( assert entry.translation_key == "conversation" +@pytest.mark.usefixtures("mock_init_component") async def test_error_handling( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test error handling.""" @@ -257,8 +255,9 @@ async def test_template_variables( assert "The user id is 12345." in mock_create_stream.call_args.kwargs["system"] +@pytest.mark.usefixtures("mock_init_component") async def test_conversation_agent( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component + hass: HomeAssistant, ) -> None: """Test Anthropic Agent.""" agent = conversation.agent_manager.async_get_agent( @@ -267,10 +266,9 @@ async def test_conversation_agent( assert agent.supported_languages == "*" +@pytest.mark.usefixtures("mock_init_component") async def test_token_stats_reported( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component: None, ) -> None: """Test that cache reads, not cache creation, are reported as cached tokens.""" trace.async_clear_traces() @@ -332,10 +330,9 @@ async def test_token_stats_reported( } +@pytest.mark.usefixtures("mock_init_component") async def test_prompt_caching_system_prompt( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component: None, mock_create_stream: AsyncMock, ) -> None: """Ensure system prompt is sent as TextBlockParam with cache_control.""" @@ -363,10 +360,10 @@ async def test_prompt_caching_system_prompt( assert "cache_control" not in mock_create_stream.call_args.kwargs +@pytest.mark.usefixtures("mock_init_component") async def test_prompt_caching_automatic( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component: None, mock_create_stream: AsyncMock, ) -> None: """Ensure model args include cache_control.""" @@ -402,26 +399,25 @@ async def test_prompt_caching_automatic( @pytest.mark.parametrize( ("tool_call_json_parts", "expected_call_tool_args"), [ - ( - ['{"param1": "test_value"}'], - {"param1": "test_value"}, + pytest.param( + ['{"param1": "test_value"}'], {"param1": "test_value"}, id="complete_json" ), - ( + pytest.param( ['{"para', 'm1": "test_valu', 'e"}'], {"param1": "test_value"}, + id="chunked_json", ), - ([""], {}), + pytest.param([""], {}, id="empty_arguments"), ], ) @freeze_time("2024-06-03 23:00:00") +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_function_call( - mock_get_tools, + mock_get_tools: AsyncMock, hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, tool_call_json_parts: list[str], - expected_call_tool_args: dict[str, Any], + expected_call_tool_args: dict[str, str], ) -> None: """Test function call from the assistant.""" agent_id = "conversation.claude_conversation" @@ -497,11 +493,10 @@ async def test_function_call( @patch("homeassistant.components.llm.async_get_tools", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_function_exception( - mock_get_tools, + mock_get_tools: AsyncMock, hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test function call with exception.""" @@ -574,10 +569,9 @@ async def test_function_exception( ) +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_assist_api_tools_conversion( hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test that we are able to convert actual tools from Assist API.""" @@ -623,11 +617,11 @@ async def test_assist_api_tools_conversion( ) +@pytest.mark.usefixtures("mock_init_component") async def test_unknown_hass_api( hass: HomeAssistant, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, - mock_init_component, ) -> None: """Test when we reference an API that no longer exists.""" subentry = next(iter(mock_config_entry.subentries.values())) @@ -648,10 +642,9 @@ async def test_unknown_hass_api( assert result == snapshot +@pytest.mark.usefixtures("mock_init_component") async def test_conversation_id( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test conversation ID is honored.""" @@ -699,10 +692,9 @@ async def test_conversation_id( assert result.conversation_id == "koala" +@pytest.mark.usefixtures("mock_init_component") async def test_refusal( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test refusal due to potential policy violation.""" @@ -729,10 +721,9 @@ async def test_refusal( ) +@pytest.mark.usefixtures("mock_init_component") async def test_stream_wrong_type( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test error if the response is not a stream.""" @@ -758,11 +749,11 @@ async def test_stream_wrong_type( assert result.response.speech["plain"]["speech"] == "Expected a stream of messages" +@pytest.mark.usefixtures( + "mock_config_entry_with_assist", "mock_init_component", "mock_create_stream" +) async def test_double_system_messages( hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, - mock_create_stream: AsyncMock, ) -> None: """Test error for two or more system prompts.""" conversation_id = "conversation_id" @@ -791,10 +782,10 @@ async def test_double_system_messages( ) +@pytest.mark.usefixtures("mock_init_component") async def test_extended_thinking( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -847,26 +838,32 @@ async def test_extended_thinking( @pytest.mark.parametrize( "subentry_data", [ - { - CONF_LLM_HASS_API: "assist", - CONF_CHAT_MODEL: "claude-haiku-4-5", - CONF_THINKING_BUDGET: 0, - }, - { - CONF_LLM_HASS_API: "assist", - CONF_CHAT_MODEL: "claude-opus-4-7", - CONF_THINKING_EFFORT: "none", - }, + pytest.param( + { + CONF_LLM_HASS_API: "assist", + CONF_CHAT_MODEL: "claude-haiku-4-5", + CONF_THINKING_BUDGET: 0, + }, + id="zero_budget", + ), + pytest.param( + { + CONF_LLM_HASS_API: "assist", + CONF_CHAT_MODEL: "claude-opus-4-7", + CONF_THINKING_EFFORT: "none", + }, + id="no_effort", + ), ], ) @freeze_time("2024-05-24 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_disabled_thinking( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, - subentry_data: dict[str, Any], + subentry_data: dict[str, str | int], ) -> None: """Test conversation with thinking effort disabled.""" hass.config_entries.async_update_subentry( @@ -895,10 +892,9 @@ async def test_disabled_thinking( @freeze_time("2024-05-24 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_redacted_thinking( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -929,11 +925,11 @@ async def test_redacted_thinking( @patch("homeassistant.components.llm.async_get_tools", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_init_component") async def test_extended_thinking_tool_call( - mock_get_tools, + mock_get_tools: AsyncMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1009,10 +1005,10 @@ async def test_extended_thinking_tool_call( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_web_search( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1155,10 +1151,10 @@ async def test_web_search( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_web_search_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1233,10 +1229,10 @@ async def test_web_search_error( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_web_search_dynamic_filtering( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1377,10 +1373,10 @@ async def test_web_search_dynamic_filtering( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_bash_code_execution( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1458,10 +1454,10 @@ async def test_bash_code_execution( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_bash_code_execution_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1535,7 +1531,7 @@ async def test_bash_code_execution_error( @pytest.mark.parametrize( ("args_parts", "content"), [ - ( + pytest.param( [ "", '{"', @@ -1551,8 +1547,9 @@ async def test_bash_code_execution_error( TextEditorCodeExecutionCreateResultBlock( type="text_editor_code_execution_create_result", is_file_update=False ), + id="create_file", ), - ( + pytest.param( [ "", '{"comman', @@ -1584,8 +1581,9 @@ async def test_bash_code_execution_error( old_lines=1, old_start=1, ), + id="replace_text", ), - ( + pytest.param( [ "", '{"command', @@ -1603,8 +1601,9 @@ async def test_bash_code_execution_error( start_line=1, total_lines=1, ), + id="view_file", ), - ( + pytest.param( [ "", '{"com', @@ -1626,14 +1625,15 @@ async def test_bash_code_execution_error( " line 1 column 1 (char 0)" ), ), + id="tool_error", ), ], ) @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_text_editor_code_execution( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, args_parts: list[str], @@ -1681,10 +1681,10 @@ async def test_text_editor_code_execution( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_tool_search( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1798,10 +1798,10 @@ async def test_tool_search( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_tool_search_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1871,10 +1871,10 @@ async def test_tool_search_error( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_web_fetch( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -1978,10 +1978,10 @@ async def test_web_fetch( @freeze_time("2025-10-31 12:00:00") +@pytest.mark.usefixtures("mock_init_component") async def test_web_fetch_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, ) -> None: @@ -2048,10 +2048,9 @@ async def test_web_fetch_error( assert mock_create_stream.call_args.kwargs["messages"] == snapshot +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_container_reused( hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test that container is reused.""" @@ -2104,370 +2103,407 @@ async def test_container_reused( @pytest.mark.parametrize( "content", [ - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What shape is a donut?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="A donut is a torus.", - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What shape is a donut?"), - conversation.chat_log.UserContent("Can you tell me?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="A donut is a torus.", - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", content="Hope this helps." - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What shape is a donut?"), - conversation.chat_log.UserContent("Can you tell me?"), - conversation.chat_log.UserContent("Please?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="A donut is a torus.", - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", content="Hope this helps." - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", content="You are welcome." - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("Turn off the lights and make me coffee"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Sure.", - tool_calls=[ - llm.ToolInput( - id="mock-tool-call-id", - tool_name="HassTurnOff", - tool_args={"domain": "light"}, - ), - llm.ToolInput( - id="mock-tool-call-id-2", - tool_name="MakeCoffee", - tool_args={}, - ), - ], - ), - conversation.chat_log.UserContent("Thank you"), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="mock-tool-call-id", - tool_name="HassTurnOff", - result=llm.ToolResult( - data={"success": True, "response": "Lights are off."} + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + ], + id="system_only", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What shape is a donut?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="A donut is a torus.", ), - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="mock-tool-call-id-2", - tool_name="MakeCoffee", - result=llm.ToolResult( - data={"success": False, "response": "Not enough milk."} + ], + id="single_exchange", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What shape is a donut?"), + conversation.chat_log.UserContent("Can you tell me?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="A donut is a torus.", ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Should I add milk to the shopping list?", - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What's on the news today?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="To get today's news, I'll perform a web search", - thinking_content=( - "The user is asking about today's news," - " which requires current, real-time" - " information. This is clearly something" - " that requires recent information beyond" - " my knowledge cutoff. I should use the" - " web_search tool to find today's news." + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Hope this helps.", ), - native=ContentDetails(thinking_signature="ErU/V+ayA=="), - tool_calls=[ - llm.ToolInput( - id="srvtoolu_12345ABC", - tool_name="web_search", - tool_args={"query": "today's news"}, - external=True, - ), - ], - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="srvtoolu_12345ABC", - tool_name="web_search", - result=llm.ToolResult( - data={ - "content": [ - { - "type": "web_search_result", - "title": "Today's News - Example.com", - "url": "https://www.example.com/todays-news", - "page_age": "2 days ago", - "encrypted_content": "ABCDEFG", - }, - { - "type": "web_search_result", - "title": "Breaking News - NewsSite.com", - "url": "https://www.newssite.com/breaking-news", - "page_age": None, - "encrypted_content": "ABCDEFG", - }, - ] - } + ], + id="two_consecutive_messages", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What shape is a donut?"), + conversation.chat_log.UserContent("Can you tell me?"), + conversation.chat_log.UserContent("Please?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="A donut is a torus.", ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Here's what I found on the web about today's news:\n" - "1. New Home Assistant release\n" - "2. Something incredible happened\n" - "Those are the main headlines making news today.", - native=ContentDetails( - citation_details=[ - CitationDetails( - index=54, - length=26, - citations=[ - CitationWebSearchResultLocationParam( - type="web_search_result_location", - cited_text=( - "This release iterates on some of" - " the features we introduced in" - " the last couple of releases," - " but also..." - ), - encrypted_index="AAA==", - title="Home Assistant Release", - url="https://www.example.com/todays-news", - ), - ], + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Hope this helps.", + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="You are welcome.", + ), + ], + id="three_consecutive_messages", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent( + "Turn off the lights and make me coffee" + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Sure.", + tool_calls=[ + llm.ToolInput( + id="mock-tool-call-id", + tool_name="HassTurnOff", + tool_args={"domain": "light"}, ), - CitationDetails( - index=84, - length=29, - citations=[ - CitationWebSearchResultLocationParam( - type="web_search_result_location", - cited_text=( - "Breaking news from around the" - " world today includes major" - " events in technology, politics," - " and culture..." - ), - encrypted_index="AQE=", - title="Breaking News", - url="https://www.newssite.com/breaking-news", - ), - CitationWebSearchResultLocationParam( - type="web_search_result_location", - cited_text="Well, this happened...", - encrypted_index="AgI=", - title="Breaking News", - url="https://www.newssite.com/breaking-news", - ), - ], + llm.ToolInput( + id="mock-tool-call-id-2", + tool_name="MakeCoffee", + tool_args={}, ), ], ), - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What's new in Home Assistant?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Sure, let me check that for you!", - thinking_content="I need to use the web_fetch tool to fetch the latest release notes from the Home Assistant website.", - native=ContentDetails(thinking_signature="ErU/V+ayA=="), - tool_calls=[ - llm.ToolInput( - id="srvtoolu_12345ABC", - tool_name="web_fetch", - tool_args={ - "url": "https://www.home-assistant.io/latest-release-notes/" - }, - external=True, + conversation.chat_log.UserContent("Thank you"), + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="mock-tool-call-id", + tool_name="HassTurnOff", + result=llm.ToolResult( + data={"success": True, "response": "Lights are off."} ), - ], - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="srvtoolu_12345ABC", - tool_name="web_fetch", - result=llm.ToolResult( - data={ - "type": "web_fetch_result", - "url": "https://www.home-assistant.io/latest-release-notes/", - "content": { - "type": "document", - "source": { - "type": "text", - "media_type": "text/plain", - "data": "Home Assistant new version is out!\nMany new features.\nAnthropic integration now supports web fetch tool.\nEnjoy the release!", - }, - "title": "Latest Home Assistant Release Notes", - "citations": {"enabled": True}, - }, - "retrieved_at": "2026-04-04T10:30:00Z", - } ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Here's what's great about the new release:\n" - "1. Lots of new features\n" - "2. New web fetch tool for Anthropic integration\n" - "Enjoy!", - native=ContentDetails( - citation_details=[ - CitationDetails( - index=70, - length=44, - citations=[ - CitationCharLocationParam( - type="char_location", - cited_text="Anthropic integration now supports web fetch tool.", - document_index=0, - document_title="Latest Home Assistant Release Notes", - start_char_index=56, - end_char_index=105, - ), - ], + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="mock-tool-call-id-2", + tool_name="MakeCoffee", + result=llm.ToolResult( + data={"success": False, "response": "Not enough milk."} + ), + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Should I add milk to the shopping list?", + ), + ], + id="tool_results", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What's on the news today?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="To get today's news, I'll perform a web search", + thinking_content=( + "The user is asking about today's news," + " which requires current, real-time" + " information. This is clearly something" + " that requires recent information beyond" + " my knowledge cutoff. I should use the" + " web_search tool to find today's news." + ), + native=ContentDetails(thinking_signature="ErU/V+ayA=="), + tool_calls=[ + llm.ToolInput( + id="srvtoolu_12345ABC", + tool_name="web_search", + tool_args={"query": "today's news"}, + external=True, ), ], ), - ), - ], - [ - conversation.chat_log.SystemContent("You are a helpful assistant."), - conversation.chat_log.UserContent("What time is it?"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="Let me check the time for you.", - tool_calls=[ - llm.ToolInput( - id="mock-tool-call-id", - tool_name="GetCurrentTime", - tool_args={}, + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="srvtoolu_12345ABC", + tool_name="web_search", + result=llm.ToolResult( + data={ + "content": [ + { + "type": "web_search_result", + "title": "Today's News - Example.com", + "url": "https://www.example.com/todays-news", + "page_age": "2 days ago", + "encrypted_content": "ABCDEFG", + }, + { + "type": "web_search_result", + "title": "Breaking News - NewsSite.com", + "url": "https://www.newssite.com/breaking-news", + "page_age": None, + "encrypted_content": "ABCDEFG", + }, + ] + } ), - ], - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="mock-tool-call-id", - tool_name="GetCurrentTime", - result=llm.ToolResult( - data={ - "speech_slots": {"time": datetime.time(14, 30, 0)}, - "message": "Current time retrieved", - } ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="It is currently 2:30 PM.", - ), - ], - [ - conversation.chat_log.SystemContent( - "You are a voice assistant for Home Assistant." - ), - conversation.chat_log.UserContent("Set humidity to 50%"), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - thinking_content="Let me search for a tool to set humidity.", - tool_calls=[ - llm.ToolInput( - tool_name="tool_search_tool_bm25", - tool_args={"query": "set humidity humidifier"}, - id="srvtoolu_015vXmtZNASLa7n9RsoDfcBC", - external=True, - ) - ], - native=ContentDetails(thinking_signature="EuQBClkIDBE="), - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="srvtoolu_015vXmtZNASLa7n9RsoDfcBC", - tool_name="tool_search", - result=llm.ToolResult( - data={ - "tool_references": [ - { - "tool_name": "HassHumidifierSetpoint", - "type": "tool_reference", - }, - { - "tool_name": "HassHumidifierMode", - "type": "tool_reference", - }, - { - "tool_name": "HassClimateSetTemperature", - "type": "tool_reference", - }, - {"tool_name": "HassFanSetSpeed", "type": "tool_reference"}, - {"tool_name": "HassSetVolume", "type": "tool_reference"}, + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Here's what I found on the web about today's news:\n" + "1. New Home Assistant release\n" + "2. Something incredible happened\n" + "Those are the main headlines making news today.", + native=ContentDetails( + citation_details=[ + CitationDetails( + index=54, + length=26, + citations=[ + CitationWebSearchResultLocationParam( + type="web_search_result_location", + cited_text=( + "This release iterates on some of" + " the features we introduced in" + " the last couple of releases," + " but also..." + ), + encrypted_index="AAA==", + title="Home Assistant Release", + url="https://www.example.com/todays-news", + ), + ], + ), + CitationDetails( + index=84, + length=29, + citations=[ + CitationWebSearchResultLocationParam( + type="web_search_result_location", + cited_text=( + "Breaking news from around the" + " world today includes major" + " events in technology, politics," + " and culture..." + ), + encrypted_index="AQE=", + title="Breaking News", + url="https://www.newssite.com/breaking-news", + ), + CitationWebSearchResultLocationParam( + type="web_search_result_location", + cited_text="Well, this happened...", + encrypted_index="AgI=", + title="Breaking News", + url="https://www.newssite.com/breaking-news", + ), + ], + ), ], - "type": "tool_search_tool_search_result", - } + ), ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - tool_calls=[ - llm.ToolInput( - tool_name="HassHumidifierSetpoint", - tool_args={"name": "Hygrostat", "humidity": 50}, - id="toolu_01KNRWb3ZFufCa7WXtzCakhc", - external=False, - ) - ], - ), - conversation.chat_log.ToolResultContent( - agent_id="conversation.claude_conversation", - tool_call_id="toolu_01KNRWb3ZFufCa7WXtzCakhc", - tool_name="HassHumidifierSetpoint", - result=llm.ToolResult( - data={ - "speech": { - "plain": { - "speech": "The Hygrostat is set to 50%", - "extra_data": None, - } - }, - "response_type": "action_done", - "data": {"success": [], "failed": []}, - } + ], + id="web_search_citations", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What's new in Home Assistant?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Sure, let me check that for you!", + thinking_content="I need to use the web_fetch tool to fetch the latest release notes from the Home Assistant website.", + native=ContentDetails(thinking_signature="ErU/V+ayA=="), + tool_calls=[ + llm.ToolInput( + id="srvtoolu_12345ABC", + tool_name="web_fetch", + tool_args={ + "url": "https://www.home-assistant.io/latest-release-notes/" + }, + external=True, + ), + ], ), - ), - conversation.chat_log.AssistantContent( - agent_id="conversation.claude_conversation", - content="The Hygrostat humidity has been set to **50%**. ✅", - ), - ], + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="srvtoolu_12345ABC", + tool_name="web_fetch", + result=llm.ToolResult( + data={ + "type": "web_fetch_result", + "url": "https://www.home-assistant.io/latest-release-notes/", + "content": { + "type": "document", + "source": { + "type": "text", + "media_type": "text/plain", + "data": "Home Assistant new version is out!\nMany new features.\nAnthropic integration now supports web fetch tool.\nEnjoy the release!", + }, + "title": "Latest Home Assistant Release Notes", + "citations": {"enabled": True}, + }, + "retrieved_at": "2026-04-04T10:30:00Z", + } + ), + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Here's what's great about the new release:\n" + "1. Lots of new features\n" + "2. New web fetch tool for Anthropic integration\n" + "Enjoy!", + native=ContentDetails( + citation_details=[ + CitationDetails( + index=70, + length=44, + citations=[ + CitationCharLocationParam( + type="char_location", + cited_text="Anthropic integration now supports web fetch tool.", + document_index=0, + document_title="Latest Home Assistant Release Notes", + start_char_index=56, + end_char_index=105, + ), + ], + ), + ], + ), + ), + ], + id="web_fetch_citations", + ), + pytest.param( + [ + conversation.chat_log.SystemContent("You are a helpful assistant."), + conversation.chat_log.UserContent("What time is it?"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="Let me check the time for you.", + tool_calls=[ + llm.ToolInput( + id="mock-tool-call-id", + tool_name="GetCurrentTime", + tool_args={}, + ), + ], + ), + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="mock-tool-call-id", + tool_name="GetCurrentTime", + result=llm.ToolResult( + data={ + "speech_slots": {"time": datetime.time(14, 30, 0)}, + "message": "Current time retrieved", + } + ), + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="It is currently 2:30 PM.", + ), + ], + id="time_in_tool_result", + ), + pytest.param( + [ + conversation.chat_log.SystemContent( + "You are a voice assistant for Home Assistant." + ), + conversation.chat_log.UserContent("Set humidity to 50%"), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + thinking_content="Let me search for a tool to set humidity.", + tool_calls=[ + llm.ToolInput( + tool_name="tool_search_tool_bm25", + tool_args={"query": "set humidity humidifier"}, + id="srvtoolu_015vXmtZNASLa7n9RsoDfcBC", + external=True, + ) + ], + native=ContentDetails(thinking_signature="EuQBClkIDBE="), + ), + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="srvtoolu_015vXmtZNASLa7n9RsoDfcBC", + tool_name="tool_search", + result=llm.ToolResult( + data={ + "tool_references": [ + { + "tool_name": "HassHumidifierSetpoint", + "type": "tool_reference", + }, + { + "tool_name": "HassHumidifierMode", + "type": "tool_reference", + }, + { + "tool_name": "HassClimateSetTemperature", + "type": "tool_reference", + }, + { + "tool_name": "HassFanSetSpeed", + "type": "tool_reference", + }, + { + "tool_name": "HassSetVolume", + "type": "tool_reference", + }, + ], + "type": "tool_search_tool_search_result", + } + ), + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + tool_calls=[ + llm.ToolInput( + tool_name="HassHumidifierSetpoint", + tool_args={"name": "Hygrostat", "humidity": 50}, + id="toolu_01KNRWb3ZFufCa7WXtzCakhc", + external=False, + ) + ], + ), + conversation.chat_log.ToolResultContent( + agent_id="conversation.claude_conversation", + tool_call_id="toolu_01KNRWb3ZFufCa7WXtzCakhc", + tool_name="HassHumidifierSetpoint", + result=llm.ToolResult( + data={ + "speech": { + "plain": { + "speech": "The Hygrostat is set to 50%", + "extra_data": None, + } + }, + "response_type": "action_done", + "data": {"success": [], "failed": []}, + } + ), + ), + conversation.chat_log.AssistantContent( + agent_id="conversation.claude_conversation", + content="The Hygrostat humidity has been set to **50%**. ✅", + ), + ], + id="tool_search_results", + ), ], ) +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_history_conversion( hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, snapshot: SnapshotAssertion, content: list[conversation.chat_log.Content], @@ -2492,10 +2528,9 @@ async def test_history_conversion( assert mock_create_stream.mock_calls[0][2]["messages"] == snapshot +@pytest.mark.usefixtures("mock_config_entry_with_assist", "mock_init_component") async def test_history_conversion_skips_whitespace_content( hass: HomeAssistant, - mock_config_entry_with_assist: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test that whitespace-only chat log content is not sent to the API. diff --git a/tests/components/anthropic/test_coordinator.py b/tests/components/anthropic/test_coordinator.py index 5a9a35fce01f..d5d8d475b5fb 100644 --- a/tests/components/anthropic/test_coordinator.py +++ b/tests/components/anthropic/test_coordinator.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from anthropic import APITimeoutError, AuthenticationError, RateLimitError from freezegun import freeze_time from httpx import URL, Request, Response +import pytest from homeassistant.components import conversation from homeassistant.components.anthropic.const import DOMAIN @@ -21,11 +22,11 @@ from tests.common import MockConfigEntry, async_fire_time_changed @patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_init_component") async def test_auth_error_handling( mock_model_list: AsyncMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test reauth after authentication error during conversation.""" @@ -64,11 +65,10 @@ async def test_auth_error_handling( @freeze_time("2026-02-27 12:00:00") @patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_init_component") async def test_connection_error_handling( mock_model_list: AsyncMock, hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test making entity unavailable on connection error.""" @@ -133,11 +133,11 @@ async def test_connection_error_handling( @patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_init_component") async def test_connection_check_reauth( mock_model_list: AsyncMock, hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, ) -> None: """Test authentication error during background availability check.""" mock_model_list.side_effect = APITimeoutError( @@ -197,11 +197,10 @@ async def test_connection_check_reauth( @patch("anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock) +@pytest.mark.usefixtures("mock_init_component") async def test_connection_restore( mock_model_list: AsyncMock, hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_init_component, mock_create_stream: AsyncMock, ) -> None: """Test background availability check restore on non-connectivity error.""" diff --git a/tests/components/anthropic/test_diagnostics.py b/tests/components/anthropic/test_diagnostics.py index 44b6bdc73c04..2b8fc89830b4 100644 --- a/tests/components/anthropic/test_diagnostics.py +++ b/tests/components/anthropic/test_diagnostics.py @@ -1,5 +1,6 @@ """Test Anthropic diagnostics.""" +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant @@ -9,11 +10,11 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator +@pytest.mark.usefixtures("mock_init_component") async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_config_entry: MockConfigEntry, - mock_init_component: None, snapshot: SnapshotAssertion, ) -> None: """Test config entry diagnostics.""" diff --git a/tests/components/anthropic/test_init.py b/tests/components/anthropic/test_init.py index bb5301e5890a..3c6041c7594c 100644 --- a/tests/components/anthropic/test_init.py +++ b/tests/components/anthropic/test_init.py @@ -1,10 +1,11 @@ """Tests for the Anthropic integration.""" -from typing import Any +from typing import TypedDict from unittest.mock import patch from anthropic import ( APIConnectionError, + APIError, APITimeoutError, AuthenticationError, BadRequestError, @@ -37,12 +38,23 @@ from tests.common import MockConfigEntry MINOR_VERSION = AnthropicConfigFlow.MINOR_VERSION +class ConversationSubentryExpectation(TypedDict): + """Expected registry state for a migrated conversation subentry.""" + + conversation_entity_id: str + device_disabled_by: DeviceEntryDisabler | None + entity_disabled_by: RegistryEntryDisabler | None + device: int + + @pytest.mark.parametrize( ("side_effect", "error"), [ - (APIConnectionError(request=None), "Connection error"), - (APITimeoutError(request=None), "Request timed out"), - ( + pytest.param( + APIConnectionError(request=None), "Connection error", id="connection_error" + ), + pytest.param(APITimeoutError(request=None), "Request timed out", id="timeout"), + pytest.param( BadRequestError( message=( "Your credit balance is too low to access" @@ -56,15 +68,16 @@ MINOR_VERSION = AnthropicConfigFlow.MINOR_VERSION body={"type": "error", "error": {"type": "invalid_request_error"}}, ), "Your credit balance is too low to access the Claude API", + id="insufficient_credit", ), ], ) +@pytest.mark.usefixtures("mock_config_entry") async def test_init_error( hass: HomeAssistant, - mock_config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, - side_effect, - error, + side_effect: APIError, + error: str, ) -> None: """Test initialization errors.""" with patch( @@ -96,10 +109,10 @@ async def test_init_auth_error( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR +@pytest.mark.usefixtures("mock_init_component") async def test_init_repair_issue( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_init_component, issue_registry: ir.IssueRegistry, ) -> None: """Test that repair issue is created on deprecated model.""" @@ -261,7 +274,7 @@ async def test_migration_from_v1_to_v2( "main_config_entry", ), [ - ( + pytest.param( [ConfigEntryDisabler.USER, None], [DeviceEntryDisabler.CONFIG_ENTRY, None], [RegistryEntryDisabler.CONFIG_ENTRY, None], @@ -281,8 +294,9 @@ async def test_migration_from_v1_to_v2( }, ], 1, + id="first_entry_disabled", ), - ( + pytest.param( [None, ConfigEntryDisabler.USER], [None, DeviceEntryDisabler.CONFIG_ENTRY], [None, RegistryEntryDisabler.CONFIG_ENTRY], @@ -302,8 +316,9 @@ async def test_migration_from_v1_to_v2( }, ], 0, + id="second_entry_disabled", ), - ( + pytest.param( [ConfigEntryDisabler.USER, ConfigEntryDisabler.USER], [DeviceEntryDisabler.CONFIG_ENTRY, DeviceEntryDisabler.CONFIG_ENTRY], [RegistryEntryDisabler.CONFIG_ENTRY, RegistryEntryDisabler.CONFIG_ENTRY], @@ -323,6 +338,7 @@ async def test_migration_from_v1_to_v2( }, ], 0, + id="both_entries_disabled", ), ], ) @@ -335,7 +351,7 @@ async def test_migration_from_v1_disabled( device_disabled_by: list[DeviceEntryDisabler | None], entity_disabled_by: list[RegistryEntryDisabler | None], merged_config_entry_disabled_by: ConfigEntryDisabler | None, - conversation_subentry_data: list[dict[str, Any]], + conversation_subentry_data: list[ConversationSubentryExpectation], main_config_entry: int, ) -> None: """Test migration where the config entries are disabled.""" @@ -807,7 +823,7 @@ async def test_migration_from_v2_1_to_v2_2( ), [ # Config entry not disabled, update device and entity disabled by config entry - ( + pytest.param( None, DeviceEntryDisabler.CONFIG_ENTRY, RegistryEntryDisabler.CONFIG_ENTRY, @@ -816,8 +832,9 @@ async def test_migration_from_v2_1_to_v2_2( None, DeviceEntryDisabler.USER, RegistryEntryDisabler.DEVICE, + id="enabled_entry_stale_flags", ), - ( + pytest.param( None, DeviceEntryDisabler.USER, RegistryEntryDisabler.DEVICE, @@ -826,8 +843,9 @@ async def test_migration_from_v2_1_to_v2_2( None, DeviceEntryDisabler.USER, RegistryEntryDisabler.DEVICE, + id="enabled_entry_device_disabled", ), - ( + pytest.param( None, DeviceEntryDisabler.USER, RegistryEntryDisabler.USER, @@ -836,8 +854,9 @@ async def test_migration_from_v2_1_to_v2_2( None, DeviceEntryDisabler.USER, RegistryEntryDisabler.USER, + id="enabled_entry_entity_disabled", ), - ( + pytest.param( None, None, None, @@ -846,9 +865,10 @@ async def test_migration_from_v2_1_to_v2_2( None, None, None, + id="enabled_entry_no_disabled_flags", ), # Config entry disabled, migration does not run - ( + pytest.param( ConfigEntryDisabler.USER, DeviceEntryDisabler.CONFIG_ENTRY, RegistryEntryDisabler.CONFIG_ENTRY, @@ -857,8 +877,9 @@ async def test_migration_from_v2_1_to_v2_2( ConfigEntryDisabler.USER, DeviceEntryDisabler.CONFIG_ENTRY, RegistryEntryDisabler.CONFIG_ENTRY, + id="disabled_entry_config_entry_flags", ), - ( + pytest.param( ConfigEntryDisabler.USER, DeviceEntryDisabler.USER, RegistryEntryDisabler.DEVICE, @@ -867,8 +888,9 @@ async def test_migration_from_v2_1_to_v2_2( ConfigEntryDisabler.USER, DeviceEntryDisabler.USER, RegistryEntryDisabler.DEVICE, + id="disabled_entry_device_disabled", ), - ( + pytest.param( ConfigEntryDisabler.USER, DeviceEntryDisabler.USER, RegistryEntryDisabler.USER, @@ -877,8 +899,9 @@ async def test_migration_from_v2_1_to_v2_2( ConfigEntryDisabler.USER, DeviceEntryDisabler.USER, RegistryEntryDisabler.USER, + id="disabled_entry_entity_disabled", ), - ( + pytest.param( ConfigEntryDisabler.USER, None, None, @@ -887,6 +910,7 @@ async def test_migration_from_v2_1_to_v2_2( ConfigEntryDisabler.USER, None, None, + id="disabled_entry_no_disabled_flags", ), ], ) @@ -901,7 +925,7 @@ async def test_migrate_entry_to_v2_3( setup_result: bool, minor_version_after_migration: int, config_entry_disabled_by_after_migration: ConfigEntryDisabler | None, - device_disabled_by_after_migration: ConfigEntryDisabler | None, + device_disabled_by_after_migration: DeviceEntryDisabler | None, entity_disabled_by_after_migration: RegistryEntryDisabler | None, ) -> None: """Test migration to version 2.3.""" diff --git a/tests/components/anthropic/test_repairs.py b/tests/components/anthropic/test_repairs.py index 7200c97d9ca9..7aa697845f8a 100644 --- a/tests/components/anthropic/test_repairs.py +++ b/tests/components/anthropic/test_repairs.py @@ -1,13 +1,16 @@ """Tests for the Anthropic repairs flow.""" from types import SimpleNamespace -from typing import Any from unittest.mock import AsyncMock, MagicMock from anthropic.pagination import AsyncPage from homeassistant.components.anthropic.const import CONF_CHAT_MODEL, DOMAIN -from homeassistant.config_entries import ConfigEntryState, ConfigSubentry +from homeassistant.config_entries import ( + ConfigEntryState, + ConfigSubentry, + ConfigSubentryData, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import issue_registry as ir @@ -25,7 +28,7 @@ def _make_entry( *, title: str, api_key: str, - subentries_data: list[dict[str, Any]], + subentries_data: list[ConfigSubentryData], ) -> MockConfigEntry: """Create a config entry with subentries and runtime data.""" entry = MockConfigEntry(