diff --git a/tests/tools/test_homeassistant_tool.py b/tests/tools/test_homeassistant_tool.py index e18dcb385..654424a0a 100644 --- a/tests/tools/test_homeassistant_tool.py +++ b/tests/tools/test_homeassistant_tool.py @@ -5,6 +5,7 @@ handler validation, and availability gating. """ import json +from unittest.mock import patch import pytest @@ -304,6 +305,60 @@ class TestEntityIdValidation: assert "Invalid entity_id" not in result["error"] +# --------------------------------------------------------------------------- +# String-data deserialization (XML tool calling workaround) +# --------------------------------------------------------------------------- + + +class TestCallServiceStringData: + """data param may arrive as a JSON string (XML tool calling mode).""" + + @patch("tools.homeassistant_tool._run_async", return_value={"success": True}) + def test_string_data_deserialized(self, mock_run): + """JSON string data is parsed into a dict before dispatch.""" + _handle_call_service({ + "domain": "climate", + "service": "set_hvac_mode", + "entity_id": "climate.living_room", + "data": '{"hvac_mode": "heat"}', + }) + call_args = mock_run.call_args[0][0] # the coroutine arg + # _run_async was called, meaning we got past validation + + @patch("tools.homeassistant_tool._run_async", return_value={"success": True}) + def test_dict_data_passthrough(self, mock_run): + """Dict data (JSON tool calling mode) still works unchanged.""" + _handle_call_service({ + "domain": "light", + "service": "turn_on", + "entity_id": "light.bedroom", + "data": {"brightness": 255}, + }) + mock_run.assert_called_once() + + def test_invalid_json_string_returns_error(self): + """Malformed JSON string in data returns a clear error.""" + result = json.loads(_handle_call_service({ + "domain": "light", + "service": "turn_on", + "entity_id": "light.bedroom", + "data": "{not valid json}", + })) + assert "error" in result + assert "Invalid JSON" in result["error"] + + @patch("tools.homeassistant_tool._run_async", return_value={"success": True}) + def test_empty_string_data_becomes_none(self, mock_run): + """Empty/whitespace string data is treated as None.""" + _handle_call_service({ + "domain": "light", + "service": "turn_on", + "entity_id": "light.bedroom", + "data": " ", + }) + mock_run.assert_called_once() + + # --------------------------------------------------------------------------- # Security: domain/service name format validation # --------------------------------------------------------------------------- diff --git a/tools/homeassistant_tool.py b/tools/homeassistant_tool.py index 757aeb088..2e698a459 100644 --- a/tools/homeassistant_tool.py +++ b/tools/homeassistant_tool.py @@ -458,10 +458,10 @@ HA_CALL_SERVICE_SCHEMA = { "data": { "type": "string", "description": ( - "Additional service data provided as a valid JSON string. Examples: " - '\'{"brightness": 255, "color_name": "blue"}\' for lights, ' - '\'{"temperature": 22, "hvac_mode": "heat"}\' for climate, ' - '\'{"volume_level": 0.5}\' for media players.' + "Additional service data as a JSON string. Examples: " + '{"brightness": 255, "color_name": "blue"} for lights, ' + '{"temperature": 22, "hvac_mode": "heat"} for climate, ' + '{"volume_level": 0.5} for media players.' ), }, },