@@ -4126,7 +4126,13 @@ async def test_addon_mode_uses_direct_supervisor_rest(self, monkeypatch):
41264126 "success" : True ,
41274127 "result" : {"addons" : [{"slug" : "core_mqtt" }]},
41284128 }
4129- factory .assert_called_once_with (timeout = 30.0 , verify = False )
4129+ supervisor_timeout = factory .call_args .kwargs ["timeout" ]
4130+ assert isinstance (supervisor_timeout , httpx .Timeout )
4131+ assert supervisor_timeout .connect == 10.0
4132+ assert supervisor_timeout .read == 30.0
4133+ assert supervisor_timeout .write == 30.0
4134+ assert supervisor_timeout .pool == 10.0
4135+ assert factory .call_args .kwargs ["verify" ] is False
41304136 direct_client .request .assert_awaited_once_with ("GET" , "/addons" )
41314137 client .send_websocket_message .assert_not_awaited ()
41324138
@@ -4278,6 +4284,11 @@ async def test_addon_mode_preserves_direct_http_status_classification(
42784284 assert "refresh" in suggestions
42794285 assert "hassio_api" in suggestions
42804286 assert "hassio_role" in suggestions
4287+ if status_code == 404 :
4288+ assert (
4289+ payload ["error" ]["suggestion" ]
4290+ == "Check Home Assistant connection and Supervisor availability"
4291+ )
42814292
42824293 client .send_websocket_message .assert_not_awaited ()
42834294
@@ -4370,6 +4381,8 @@ async def test_addon_mode_pre_send_write_failure_is_not_ambiguous(
43704381
43714382 payload = _parse_tool_error (exc_info )
43724383 assert payload ["error" ]["code" ] == expected_code
4384+ if isinstance (transport_error , (httpx .ConnectTimeout , httpx .PoolTimeout )):
4385+ assert "10.0s connection-acquisition timeout" in payload ["error" ]["message" ]
43734386 assert "outcome" not in payload
43744387 direct_client .request .assert_awaited_once ()
43754388 client .send_websocket_message .assert_not_awaited ()
@@ -4512,6 +4525,77 @@ async def test_addon_mode_write_server_error_reports_unknown_outcome(
45124525 assert payload ["status_code" ] == response .status_code
45134526 assert payload ["response_body" ] == response .text
45144527
4528+ @pytest .mark .asyncio
4529+ async def test_addon_mode_write_server_error_caps_response_body (self , monkeypatch ):
4530+ """An unknown-outcome error never returns an unbounded response body."""
4531+ from ha_mcp .tools .tools_addons import _supervisor_api_call
4532+
4533+ monkeypatch .setenv ("SUPERVISOR_TOKEN" , "test-supervisor-token" )
4534+ client = _make_mock_client ()
4535+ client .send_websocket_message = AsyncMock ()
4536+ direct_client = AsyncMock ()
4537+ direct_client .request .return_value = httpx .Response (
4538+ 503 ,
4539+ text = "x" * (50 * 1024 + 1 ),
4540+ )
4541+ context = MagicMock ()
4542+ context .__aenter__ = AsyncMock (return_value = direct_client )
4543+ context .__aexit__ = AsyncMock (return_value = False )
4544+
4545+ with (
4546+ patch (
4547+ "ha_mcp.tools.tools_addons.make_supervisor_httpx_client" ,
4548+ return_value = context ,
4549+ create = True ,
4550+ ),
4551+ pytest .raises (ToolError ) as exc_info ,
4552+ ):
4553+ await _supervisor_api_call (
4554+ client ,
4555+ "/addons/core_mosquitto/restart" ,
4556+ method = "POST" ,
4557+ )
4558+
4559+ payload = _parse_tool_error (exc_info )
4560+ assert payload ["response_body" ] == "x" * (50 * 1024 )
4561+
4562+ @pytest .mark .asyncio
4563+ async def test_addon_mode_write_redirect_reports_unknown_outcome (self , monkeypatch ):
4564+ """A redirect cannot prove whether Supervisor applied the received write."""
4565+ from ha_mcp .tools .tools_addons import _supervisor_api_call
4566+
4567+ monkeypatch .setenv ("SUPERVISOR_TOKEN" , "test-supervisor-token" )
4568+ client = _make_mock_client ()
4569+ client .send_websocket_message = AsyncMock ()
4570+ direct_client = AsyncMock ()
4571+ direct_client .request .return_value = httpx .Response (
4572+ 307 ,
4573+ headers = {"location" : "/addons/core_mosquitto/restart" },
4574+ )
4575+ context = MagicMock ()
4576+ context .__aenter__ = AsyncMock (return_value = direct_client )
4577+ context .__aexit__ = AsyncMock (return_value = False )
4578+
4579+ with (
4580+ patch (
4581+ "ha_mcp.tools.tools_addons.make_supervisor_httpx_client" ,
4582+ return_value = context ,
4583+ create = True ,
4584+ ),
4585+ pytest .raises (ToolError ) as exc_info ,
4586+ ):
4587+ await _supervisor_api_call (
4588+ client ,
4589+ "/addons/core_mosquitto/restart" ,
4590+ method = "POST" ,
4591+ )
4592+
4593+ payload = _parse_tool_error (exc_info )
4594+ assert payload ["error" ]["code" ] == "SERVICE_CALL_FAILED"
4595+ assert payload ["method" ] == "POST"
4596+ assert payload ["outcome" ] == "unknown"
4597+ assert payload ["status_code" ] == 307
4598+
45154599 @pytest .mark .asyncio
45164600 @pytest .mark .parametrize (
45174601 "response" ,
@@ -4826,7 +4910,13 @@ async def test_addon_long_timeout_extends_direct_rest_wait(self, monkeypatch):
48264910 timeout = 1800 ,
48274911 )
48284912
4829- factory .assert_called_once_with (timeout = 1815.0 , verify = client .verify_ssl )
4913+ supervisor_timeout = factory .call_args .kwargs ["timeout" ]
4914+ assert isinstance (supervisor_timeout , httpx .Timeout )
4915+ assert supervisor_timeout .connect == 10.0
4916+ assert supervisor_timeout .read == 1815.0
4917+ assert supervisor_timeout .write == 1815.0
4918+ assert supervisor_timeout .pool == 10.0
4919+ assert factory .call_args .kwargs ["verify" ] is client .verify_ssl
48304920 direct_client .request .assert_awaited_once_with (
48314921 "POST" , "/addons/core_mosquitto/update" , json = {}
48324922 )
0 commit comments