@@ -183,7 +183,12 @@ async def test_update_with_mismatched_inner_id_is_rejected(self, mock_client):
183183
184184 @pytest .mark .asyncio
185185 async def test_update_with_matching_inner_id_proceeds (self , mock_client ):
186- """identifier and config.id both resolve to the same unique_id → ok."""
186+ """identifier and config.id both resolve to the same unique_id → ok.
187+
188+ Asserts URL and body id both equal the resolved unique_id so a future
189+ refactor that mutates one without the other (the exact failure mode
190+ this PR fixes) would regress.
191+ """
187192 mock_client ._resolve_automation_id = AsyncMock (return_value = "AAA" )
188193 mock_client ._request = AsyncMock (return_value = {"result" : "ok" })
189194 mock_client ._poll_for_automation_entity = AsyncMock (return_value = None )
@@ -196,6 +201,44 @@ async def test_update_with_matching_inner_id_proceeds(self, mock_client):
196201 assert result ["unique_id" ] == "AAA"
197202 assert result ["operation" ] == "updated"
198203 mock_client ._request .assert_called_once ()
204+ method , url = mock_client ._request .call_args .args
205+ assert method == "POST"
206+ assert url == "/config/automation/config/AAA"
207+ assert mock_client ._request .call_args .kwargs ["json" ]["id" ] == "AAA"
208+
209+ @pytest .mark .asyncio
210+ async def test_update_with_int_vs_str_id_equivalence_passes (self , mock_client ):
211+ """``config['id']`` as int matching the resolved str id is treated as
212+ a match. HA accepts both shapes and stringifies on storage, so the
213+ guard's ``str(...)`` coercion is intentional — pin it so a future
214+ tightening to strict-type compare is a deliberate decision."""
215+ mock_client ._resolve_automation_id = AsyncMock (return_value = "1234" )
216+ mock_client ._request = AsyncMock (return_value = {"result" : "ok" })
217+ mock_client ._poll_for_automation_entity = AsyncMock (return_value = None )
218+
219+ result = await mock_client .upsert_automation_config (
220+ {"id" : 1234 , "alias" : "x" , "trigger" : [], "action" : []},
221+ identifier = "automation.foo" ,
222+ )
223+
224+ assert result ["unique_id" ] == "1234"
225+ mock_client ._request .assert_called_once ()
226+
227+ @pytest .mark .asyncio
228+ async def test_update_with_empty_string_inner_id_is_rejected (self , mock_client ):
229+ """``config['id']=''`` is a real agent-slip pattern (templating with an
230+ empty variable). It is not None, so the guard fires and rejects."""
231+ mock_client ._resolve_automation_id = AsyncMock (return_value = "AAA" )
232+ mock_client ._request = AsyncMock ()
233+
234+ with pytest .raises (HomeAssistantAPIError ) as exc_info :
235+ await mock_client .upsert_automation_config (
236+ {"id" : "" , "alias" : "x" , "trigger" : [], "action" : []},
237+ identifier = "automation.foo" ,
238+ )
239+
240+ assert exc_info .value .status_code == 400
241+ mock_client ._request .assert_not_called ()
199242
200243 @pytest .mark .asyncio
201244 async def test_update_without_inner_id_proceeds (self , mock_client ):
0 commit comments