"""Test the Anthropic config flow.""" import datetime from unittest.mock import AsyncMock, patch from anthropic import ( APIConnectionError, APIResponseValidationError, APITimeoutError, AuthenticationError, BadRequestError, InternalServerError, NotFoundError, types, ) from anthropic.types import ModelInfo from httpx import URL, Request, Response import pytest from syrupy.assertion import SnapshotAssertion from homeassistant import config_entries from homeassistant.components.anthropic.config_flow import ( DEFAULT_AI_TASK_OPTIONS, DEFAULT_CONVERSATION_OPTIONS, ) from homeassistant.components.anthropic.const import ( CONF_CHAT_MODEL, CONF_CODE_EXECUTION, CONF_MAX_TOKENS, CONF_PROMPT, CONF_PROMPT_CACHING, CONF_RECOMMENDED, CONF_THINKING_BUDGET, CONF_THINKING_EFFORT, CONF_TOOL_SEARCH, CONF_WEB_SEARCH, CONF_WEB_SEARCH_CITY, CONF_WEB_SEARCH_COUNTRY, CONF_WEB_SEARCH_MAX_USES, CONF_WEB_SEARCH_REGION, CONF_WEB_SEARCH_TIMEZONE, CONF_WEB_SEARCH_USER_LOCATION, DEFAULT, DEFAULT_AI_TASK_NAME, DEFAULT_CONVERSATION_NAME, DOMAIN, ) from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] is None with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { "api_key": "bla", }, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["data"] == { "api_key": "bla", } assert result2["options"] == {} assert result2["subentries"] == [ { "subentry_type": "conversation", "data": DEFAULT_CONVERSATION_OPTIONS, "title": DEFAULT_CONVERSATION_NAME, "unique_id": None, }, { "subentry_type": "ai_task_data", "data": DEFAULT_AI_TASK_OPTIONS, "title": DEFAULT_AI_TASK_NAME, "unique_id": None, }, ] assert len(mock_setup_entry.mock_calls) == 1 async def test_duplicate_entry(hass: HomeAssistant, mock_config_entry) -> None: """Test we abort on duplicate config entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_API_KEY: mock_config_entry.data[CONF_API_KEY], }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_creating_conversation_subentry( hass: HomeAssistant, mock_config_entry, mock_init_component ) -> None: """Test creating a conversation subentry.""" result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "conversation"), context={"source": config_entries.SOURCE_USER}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert not result["errors"] result2 = await hass.config_entries.subentries.async_configure( result["flow_id"], {CONF_NAME: "Mock name", **DEFAULT_CONVERSATION_OPTIONS}, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "Mock name" processed_options = DEFAULT_CONVERSATION_OPTIONS.copy() processed_options[CONF_PROMPT] = processed_options[CONF_PROMPT].strip() assert result2["data"] == processed_options 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.""" await hass.config_entries.async_unload(mock_config_entry.entry_id) with patch( "anthropic.resources.models.AsyncModels.list", return_value=[], ): result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "conversation"), context={"source": config_entries.SOURCE_USER}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "entry_not_loaded" @pytest.mark.parametrize( ("side_effect", "error"), [ (APIConnectionError(request=None), "cannot_connect"), (APITimeoutError(request=None), "timeout_connect"), ( BadRequestError( message=( "Your credit balance is too low to access" " the Claude API. Please go to Plans &" " Billing to upgrade or purchase credits." ), response=Response( status_code=400, request=Request(method="POST", url=URL()), ), body={"type": "error", "error": {"type": "invalid_request_error"}}, ), "unknown", ), ( AuthenticationError( message="invalid x-api-key", response=Response( status_code=401, request=Request(method="POST", url=URL()), ), body={"type": "error", "error": {"type": "authentication_error"}}, ), "authentication_error", ), ( InternalServerError( message=None, response=Response( status_code=500, request=Request(method="POST", url=URL()), ), body=None, ), "unknown", ), ( APIResponseValidationError( response=Response( status_code=200, request=Request(method="POST", url=URL()), ), body=None, ), "unknown", ), ], ) @pytest.mark.usefixtures("mock_setup_entry") async def test_api_error(hass: HomeAssistant, side_effect, error) -> None: """Test that we handle API errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock, side_effect=side_effect, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { "api_key": "bla", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { "api_key": "blabla", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "api_key": "blabla", } async def test_subentry_options_thinking_budget_more_than_max( hass: HomeAssistant, mock_config_entry, mock_init_component ) -> None: """Test error about thinking budget being more than max tokens.""" subentry = next(iter(mock_config_entry.subentries.values())) options = await mock_config_entry.start_subentry_reconfigure_flow( hass, subentry.subentry_id ) # Configure initial step options = await hass.config_entries.subentries.async_configure( options["flow_id"], { "prompt": "Speak like a pirate", "recommended": False, }, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "advanced" # Configure advanced step options = await hass.config_entries.subentries.async_configure( options["flow_id"], {"chat_model": "claude-sonnet-4-5"}, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "model" # Configure model step options = await hass.config_entries.subentries.async_configure( options["flow_id"], { "max_tokens": 8192, "thinking_budget": 16384, }, ) await hass.async_block_till_done() assert options["type"] is FlowResultType.FORM assert options["errors"] == {"thinking_budget": "thinking_budget_too_large"} # Try again options = await hass.config_entries.subentries.async_configure( options["flow_id"], { "max_tokens": 16384, "thinking_budget": 8192, }, ) await hass.async_block_till_done() assert options["type"] is FlowResultType.ABORT assert options["reason"] == "reconfigure_successful" assert subentry.data["max_tokens"] == 16384 assert subentry.data["thinking_budget"] == 8192 async def test_subentry_web_search_user_location( hass: HomeAssistant, mock_config_entry, mock_init_component ) -> None: """Test fetching user location.""" subentry = next(iter(mock_config_entry.subentries.values())) options_flow = await mock_config_entry.start_subentry_reconfigure_flow( hass, subentry.subentry_id ) # Configure initial step options = await hass.config_entries.subentries.async_configure( options_flow["flow_id"], { "prompt": "You are a helpful assistant", "recommended": False, }, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "advanced" # Configure advanced step options = await hass.config_entries.subentries.async_configure( options["flow_id"], { "chat_model": "claude-sonnet-4-5", }, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "model" hass.config.country = "US" hass.config.time_zone = "America/Los_Angeles" hass.states.async_set( "zone.home", "0", {"latitude": 37.7749, "longitude": -122.4194} ) with patch( "anthropic.resources.messages.AsyncMessages.create", new_callable=AsyncMock, return_value=types.Message( type="message", id="mock_message_id", role="assistant", model="claude-sonnet-4-0", usage=types.Usage(input_tokens=100, output_tokens=100), content=[ types.TextBlock( type="text", text='{"city": "San Francisco", "region": "California"}', ) ], ), ) as mock_create: # Configure model step options = await hass.config_entries.subentries.async_configure( options["flow_id"], { "max_tokens": 8192, "web_search": True, "web_search_max_uses": 5, "user_location": True, }, ) assert ( mock_create.call_args.kwargs["messages"][0]["content"] == "Where are the " "following coordinates located: (37.7749, -122.4194)?" ) assert options["type"] is FlowResultType.ABORT assert options["reason"] == "reconfigure_successful" assert subentry.data == { "chat_model": "claude-sonnet-4-5", "city": "San Francisco", "country": "US", "max_tokens": 8192, "prompt": "You are a helpful assistant", "prompt_caching": "prompt", "recommended": False, "region": "California", "thinking_budget": 1024, "timezone": "America/Los_Angeles", "tool_search": False, "user_location": True, "web_search": True, "web_search_max_uses": 5, "code_execution": False, } async def test_model_list( hass: HomeAssistant, mock_config_entry, mock_init_component, snapshot: SnapshotAssertion, ) -> None: """Test fetching and processing the list of models.""" subentry = next(iter(mock_config_entry.subentries.values())) options_flow = await mock_config_entry.start_subentry_reconfigure_flow( hass, subentry.subentry_id ) # Configure initial step options = await hass.config_entries.subentries.async_configure( options_flow["flow_id"], { "prompt": "You are a helpful assistant", "recommended": False, }, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "advanced" assert options["data_schema"].schema["chat_model"].config["options"] == snapshot async def test_invalid_model( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component: None ) -> None: """Test exceptions during fetching model info.""" options = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "conversation"), context={"source": config_entries.SOURCE_USER}, ) # Configure initial step options = await hass.config_entries.subentries.async_configure( options["flow_id"], { CONF_NAME: "Mock name", **DEFAULT_CONVERSATION_OPTIONS, CONF_RECOMMENDED: False, }, ) assert options["type"] is FlowResultType.FORM assert options["step_id"] == "advanced" # Configure advanced step but with api error with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.retrieve", new_callable=AsyncMock, side_effect=InternalServerError( message="Mock server error", response=Response( status_code=500, request=Request(method="POST", url=URL()), ), body=None, ), ): options = await hass.config_entries.subentries.async_configure( options["flow_id"], { CONF_CHAT_MODEL: "invalid-model-2-0", }, ) assert options["type"] is FlowResultType.FORM assert options["errors"] == {"chat_model": "api_error"} assert options["description_placeholders"] == {"message": "Mock server error"} # Try again with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.retrieve", new_callable=AsyncMock, side_effect=NotFoundError( message="Model not found", response=Response( status_code=404, request=Request(method="GET", url=URL()), ), body={ "type": "error", "error": { "type": "not_found_error", "message": "model: invalid-model-2-0", }, }, ), ): options = await hass.config_entries.subentries.async_configure( options["flow_id"], { CONF_CHAT_MODEL: "invalid-model-2-0", }, ) assert options["type"] is FlowResultType.FORM assert options["errors"] == {"chat_model": "model_not_found"} # Try again with a valid model with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.retrieve", new_callable=AsyncMock, return_value=ModelInfo( type="model", id="valid-model-4-5", created_at=datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC), display_name="Valid Model 4-5", ), ): options = await hass.config_entries.subentries.async_configure( options["flow_id"], { CONF_CHAT_MODEL: "valid-model-4-5", }, ) assert options["type"] is FlowResultType.FORM assert not options["errors"] assert options["step_id"] == "model" options = await hass.config_entries.subentries.async_configure( options["flow_id"], {}, ) assert options["type"] is FlowResultType.CREATE_ENTRY assert options["data"][CONF_CHAT_MODEL] == "valid-model-4-5" @pytest.mark.parametrize( ("current_options", "new_options", "expected_options"), [ ( # Test converting single llm api format to list { CONF_RECOMMENDED: True, CONF_PROMPT: "", CONF_LLM_HASS_API: "assist", }, ( { CONF_RECOMMENDED: True, CONF_PROMPT: "", CONF_LLM_HASS_API: ["assist"], }, ), { CONF_RECOMMENDED: True, CONF_PROMPT: "", CONF_LLM_HASS_API: ["assist"], }, ), ( # Model with web search options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-sonnet-4-5", CONF_PROMPT: "bla", CONF_PROMPT_CACHING: "prompt", CONF_TOOL_SEARCH: False, CONF_WEB_SEARCH: True, CONF_WEB_SEARCH_MAX_USES: 4, CONF_WEB_SEARCH_USER_LOCATION: True, CONF_WEB_SEARCH_CITY: "San Francisco", CONF_WEB_SEARCH_REGION: "California", CONF_WEB_SEARCH_COUNTRY: "US", CONF_WEB_SEARCH_TIMEZONE: "America/Los_Angeles", }, ( { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: [], }, { CONF_CHAT_MODEL: "claude-haiku-4-5", CONF_PROMPT_CACHING: "off", }, { CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, ), { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_PROMPT_CACHING: "off", CONF_CHAT_MODEL: "claude-haiku-4-5", CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS], CONF_THINKING_BUDGET: DEFAULT[CONF_THINKING_BUDGET], CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, ), ( # Model with thinking budget options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-sonnet-4-5", CONF_PROMPT: "bla", CONF_LLM_HASS_API: ["assist"], CONF_PROMPT_CACHING: "off", CONF_TOOL_SEARCH: False, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 5, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_THINKING_BUDGET: 4096, CONF_CODE_EXECUTION: True, }, ( { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", }, { CONF_CHAT_MODEL: "claude-sonnet-4-5", CONF_PROMPT_CACHING: "automatic", }, { CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_TOOL_SEARCH: True, CONF_CODE_EXECUTION: False, CONF_THINKING_BUDGET: 2048, }, ), { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_PROMPT_CACHING: "automatic", CONF_CHAT_MODEL: "claude-sonnet-4-5", CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS], CONF_THINKING_BUDGET: 2048, CONF_TOOL_SEARCH: True, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, ), ( # Model with thinking effort options { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-opus-4-6", CONF_PROMPT: "bla", CONF_PROMPT_CACHING: "automatic", CONF_TOOL_SEARCH: True, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 5, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, CONF_THINKING_EFFORT: "max", }, ( { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: [], }, { CONF_CHAT_MODEL: "claude-opus-4-7", CONF_PROMPT_CACHING: "prompt", }, { CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_TOOL_SEARCH: False, CONF_CODE_EXECUTION: True, CONF_THINKING_EFFORT: "xhigh", }, ), { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_PROMPT_CACHING: "prompt", CONF_CHAT_MODEL: "claude-opus-4-7", CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS], CONF_THINKING_EFFORT: "xhigh", CONF_TOOL_SEARCH: False, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 10, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: True, }, ), ( # Test switching from recommended to custom options { CONF_RECOMMENDED: True, CONF_PROMPT: "bla", }, ( { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_LLM_HASS_API: [], }, { CONF_CHAT_MODEL: "claude-haiku-4-5", CONF_PROMPT_CACHING: "automatic", }, {}, ), { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_PROMPT_CACHING: "automatic", CONF_CHAT_MODEL: "claude-haiku-4-5", CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS], CONF_THINKING_BUDGET: DEFAULT[CONF_THINKING_BUDGET], CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 5, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: False, }, ), ( # Test switching from custom to recommended options { CONF_RECOMMENDED: False, CONF_PROMPT: "Speak like a pirate", CONF_PROMPT_CACHING: "off", CONF_CHAT_MODEL: DEFAULT[CONF_CHAT_MODEL], CONF_MAX_TOKENS: DEFAULT[CONF_MAX_TOKENS], CONF_THINKING_BUDGET: DEFAULT[CONF_THINKING_BUDGET], CONF_TOOL_SEARCH: True, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 5, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_CODE_EXECUTION: True, }, ( { CONF_RECOMMENDED: True, CONF_LLM_HASS_API: ["assist"], CONF_PROMPT: "", }, ), { CONF_RECOMMENDED: True, CONF_LLM_HASS_API: ["assist"], CONF_PROMPT: "", }, ), ], ) async def test_subentry_options_switching( hass: HomeAssistant, mock_config_entry, mock_init_component, current_options, new_options, expected_options, ) -> None: """Test the subentry options form.""" subentry = next(iter(mock_config_entry.subentries.values())) hass.config_entries.async_update_subentry( mock_config_entry, subentry, data=current_options ) await hass.async_block_till_done() subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow( hass, subentry.subentry_id ) assert subentry_flow["step_id"] == "init" for step_options in new_options: assert subentry_flow["type"] is FlowResultType.FORM assert not subentry_flow["errors"] # Test that current options are showed as suggested values: for key in subentry_flow["data_schema"].schema: if ( isinstance(key.description, dict) and "suggested_value" in key.description and key in current_options ): current_option = current_options[key] if key == CONF_LLM_HASS_API and isinstance(current_option, str): current_option = [current_option] assert key.description["suggested_value"] == current_option # Configure current step subentry_flow = await hass.config_entries.subentries.async_configure( subentry_flow["flow_id"], step_options, ) assert "errors" not in subentry_flow assert subentry_flow["type"] is FlowResultType.ABORT assert subentry_flow["reason"] == "reconfigure_successful" assert subentry.data == expected_options 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) # Original conversation + original ai_task assert len(mock_config_entry.subentries) == 2 result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "ai_task_data"), context={"source": config_entries.SOURCE_USER}, ) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" assert not result.get("errors") result2 = await hass.config_entries.subentries.async_configure( result["flow_id"], { "name": "Custom AI Task", CONF_RECOMMENDED: True, }, ) assert result2.get("type") is FlowResultType.CREATE_ENTRY assert result2.get("title") == "Custom AI Task" assert result2.get("data") == { CONF_RECOMMENDED: True, } assert ( len(mock_config_entry.subentries) == 3 ) # Original conversation + original ai_task + new ai_task new_subentry_id = list(set(mock_config_entry.subentries) - old_subentries)[0] new_subentry = mock_config_entry.subentries[new_subentry_id] assert new_subentry.subentry_type == "ai_task_data" assert new_subentry.title == "Custom AI Task" async def test_ai_task_subentry_not_loaded( hass: HomeAssistant, mock_config_entry: MockConfigEntry, ) -> None: """Test creating an AI task subentry when entry is not loaded.""" # Don't call mock_init_component to simulate not loaded state result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "ai_task_data"), context={"source": config_entries.SOURCE_USER}, ) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "entry_not_loaded" async def test_creating_ai_task_subentry_advanced( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component, ) -> None: """Test creating an AI task subentry with advanced settings.""" result = await hass.config_entries.subentries.async_init( (mock_config_entry.entry_id, "ai_task_data"), context={"source": config_entries.SOURCE_USER}, ) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "init" # Go to advanced settings result2 = await hass.config_entries.subentries.async_configure( result["flow_id"], { "name": "Advanced AI Task", CONF_RECOMMENDED: False, }, ) assert result2.get("type") is FlowResultType.FORM assert result2.get("step_id") == "advanced" # Configure advanced settings result3 = await hass.config_entries.subentries.async_configure( result["flow_id"], { CONF_CHAT_MODEL: "claude-sonnet-4-5", }, ) assert result3.get("type") is FlowResultType.FORM assert result3.get("step_id") == "model" # Configure model settings result4 = await hass.config_entries.subentries.async_configure( result["flow_id"], { CONF_MAX_TOKENS: 1200, CONF_WEB_SEARCH: False, }, ) assert result4.get("type") is FlowResultType.CREATE_ENTRY assert result4.get("title") == "Advanced AI Task" assert result4.get("data") == { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "claude-sonnet-4-5", CONF_MAX_TOKENS: 1200, CONF_TOOL_SEARCH: False, CONF_WEB_SEARCH: False, CONF_WEB_SEARCH_MAX_USES: 5, CONF_WEB_SEARCH_USER_LOCATION: False, CONF_THINKING_BUDGET: 1024, CONF_CODE_EXECUTION: False, CONF_PROMPT_CACHING: "prompt", } @pytest.mark.usefixtures("mock_setup_entry") async def test_reauth(hass: HomeAssistant) -> None: """Test we can reauthenticate.""" # Pretend we already set up a config entry. hass.config.components.add("anthropic") mock_config_entry = MockConfigEntry( domain=DOMAIN, state=config_entries.ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.anthropic.config_flow.anthropic.resources.models.AsyncModels.list", new_callable=AsyncMock, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_API_KEY: "new_api_key", }, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_API_KEY] == "new_api_key" @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"]), ], ) async def test_reconfigure_conversation_subentry_llm_api_schema( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_init_component, current_llm_apis: list[str], suggested_llm_apis: list[str], expected_options: list[str], ) -> None: """Test llm_hass_api field values when reconfiguring a conversation subentry.""" subentry = next(iter(mock_config_entry.subentries.values())) hass.config_entries.async_update_subentry( mock_config_entry, subentry, data={CONF_LLM_HASS_API: current_llm_apis}, ) await hass.async_block_till_done() subentry_flow = await mock_config_entry.start_subentry_reconfigure_flow( hass, subentry.subentry_id ) assert subentry_flow["type"] is FlowResultType.FORM assert subentry_flow["step_id"] == "init" # Only valid LLM APIs should be suggested and shown as options schema = subentry_flow["data_schema"].schema key = next(k for k in schema if k == CONF_LLM_HASS_API) assert key.description assert key.description.get("suggested_value") == suggested_llm_apis field_schema = schema[key] assert field_schema.config assert [ opt["value"] for opt in field_schema.config.get("options") ] == expected_options