Skip to content

Commit 8597c63

Browse files
Patch76claude
andauthored
fix: join the entity registry on the ha_config_list_helpers legacy fallback (#1960)
_legacy_helper_list (the component-error fallback inside _list_helpers_via_component, reached from both the command-error and the transport-failure branches) flattened the {type}/list result but never called _enrich_helpers_with_current_registry, so a helper renamed in the UI was served with its stale storage id/name on that path, the same #1794 staleness the inline body fixes. Route the fallback result through the same registry join and thread its degrade-open warnings into the response. The all-types merge consumes _legacy_helper_list for uncovered simple types and previously dropped its (until now always-empty) warnings; propagate them too so a failed registry read is not served silently stale. Tests: assert entity_id/name/original_name on both fallback branches, and that the all-types merge surfaces the stale-join warning. Closes #1945 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 177dae1 commit 8597c63

2 files changed

Lines changed: 76 additions & 7 deletions

File tree

src/ha_mcp/tools/tools_config_helpers.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4001,11 +4001,11 @@ async def _send_component_helpers_list(
40014001
async def _legacy_helper_list(self, helper_type: str) -> dict[str, Any]:
40024002
"""Legacy ``{helper_type}/list`` success envelope, for the § 4 #3 fallback.
40034003
4004-
A faithful copy of the tool's inline legacy success path, kept separate
4005-
(rather than extracted from that body) so the #1794 registry-join PR can
4006-
patch the inline body without a conflict here. The drift is bounded to
4007-
the rare component-error fallback and is flagged by the ``warnings[]``
4008-
entry the caller appends.
4004+
A copy of the tool's inline legacy success path, kept separate rather
4005+
than extracted from that body. It flattens and joins the entity registry
4006+
(issue #1945) exactly like the inline path, so a renamed helper served on
4007+
this fallback carries its current entity_id/name; the caller additionally
4008+
appends a ``warnings[]`` entry flagging that the component path was used.
40094009
"""
40104010
result = await self._client.send_websocket_message(
40114011
{"type": f"{helper_type}/list"}
@@ -4023,13 +4023,22 @@ async def _legacy_helper_list(self, helper_type: str) -> dict[str, Any]:
40234023
# would be a dict here — breaking count, the pagination slice and the
40244024
# all-types merge, which all expect a list of records.
40254025
items = _flatten_helper_list_result(result)
4026-
return {
4026+
# Join the entity registry like the inline body (issue #1945): without
4027+
# this a renamed helper served on the component-error fallback keeps its
4028+
# stale storage id/name, the same #1794 staleness the inline path fixes.
4029+
enrich_warnings = await _enrich_helpers_with_current_registry(
4030+
self._client, helper_type, items
4031+
)
4032+
response: dict[str, Any] = {
40274033
"success": True,
40284034
"helper_type": helper_type,
40294035
"count": len(items),
40304036
"helpers": items,
40314037
"message": f"Found {len(items)} {helper_type} helper(s)",
40324038
}
4039+
if enrich_warnings:
4040+
response["warnings"] = enrich_warnings
4041+
return response
40334042

40344043
async def _list_all_helpers(self) -> dict[str, Any]:
40354044
"""Serve ``helper_type="all"``: one merged component listing, or a hard error.
@@ -4145,8 +4154,14 @@ async def _shape_all_helpers_response(
41454154
],
41464155
)
41474156
)
4157+
merge_warnings: list[str] = []
41484158
for helper_type in sorted(SIMPLE_HELPER_TYPES - covered_set):
41494159
legacy = await self._legacy_helper_list(helper_type)
4160+
# _legacy_helper_list joins the registry (issue #1945) and, degrade-
4161+
# open, flags a failed registry read in warnings[]; surface those here
4162+
# instead of dropping them, else an uncovered type is served stale and
4163+
# silent during an all-types listing.
4164+
merge_warnings.extend(legacy.get("warnings", []))
41504165
skipped = 0
41514166
for item in legacy.get("helpers", []):
41524167
if isinstance(item, dict):
@@ -4165,13 +4180,16 @@ async def _shape_all_helpers_response(
41654180
helper_type,
41664181
)
41674182

4168-
return {
4183+
response: dict[str, Any] = {
41694184
"success": True,
41704185
"helper_type": "all",
41714186
"count": len(helpers),
41724187
"helpers": helpers,
41734188
"message": f"Found {len(helpers)} helper(s)",
41744189
}
4190+
if merge_warnings:
4191+
response["warnings"] = merge_warnings
4192+
return response
41754193

41764194
@tool(
41774195
name="ha_config_set_helper",

tests/src/unit/test_ha_config_list_helpers_component_routing.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ async def send_websocket_message(self, msg: dict[str, Any]) -> dict[str, Any]:
153153
return {"success": False, "error": "unexpected list type"}
154154

155155

156+
class RegistryFailingClient(RoutingClient):
157+
"""Legacy ``{type}/list`` works, but the entity-registry read fails, so
158+
``_enrich_helpers_with_current_registry`` degrades open with a warning."""
159+
160+
async def send_websocket_message(self, msg: dict[str, Any]) -> dict[str, Any]:
161+
if msg.get("type") == "config/entity_registry/list":
162+
return {"success": False, "error": "registry unavailable"}
163+
return await super().send_websocket_message(msg)
164+
165+
156166
def _build_list_helpers(client: Any) -> Any:
157167
registered: dict[str, Any] = {}
158168

@@ -320,6 +330,13 @@ async def test_raised_command_falls_back_with_warning() -> None:
320330
assert resp["success"] is True
321331
assert client.list_calls == 1
322332
assert any("served via legacy path" in w for w in resp["warnings"])
333+
# The fallback record is registry-joined like the inline path (issue #1945):
334+
# a renamed helper carries its current entity_id and display name, not the
335+
# stale storage values.
336+
record = resp["helpers"][0]
337+
assert record["entity_id"] == "input_boolean.foo"
338+
assert record["name"] == "New Name"
339+
assert record["original_name"] == "Old Name"
323340

324341

325342
@pytest.mark.asyncio
@@ -342,6 +359,12 @@ async def test_ws_establish_failure_storage_type_falls_back_with_warning() -> No
342359
assert resp["success"] is True
343360
assert client.list_calls == 1
344361
assert any("served via legacy path" in w for w in resp["warnings"])
362+
# The transport-failure fallback joins the registry too (issue #1945): the
363+
# same _legacy_helper_list serves both this branch and the command-error one.
364+
record = resp["helpers"][0]
365+
assert record["entity_id"] == "input_boolean.foo"
366+
assert record["name"] == "New Name"
367+
assert record["original_name"] == "Old Name"
345368

346369

347370
@pytest.mark.asyncio
@@ -378,6 +401,34 @@ async def test_raised_command_fallback_flattens_the_person_split_shape() -> None
378401
assert resp["next_offset"] is None
379402

380403

404+
@pytest.mark.asyncio
405+
async def test_all_types_merge_surfaces_legacy_enrichment_warning() -> None:
406+
"""all-types: an uncovered type served via legacy whose registry read fails
407+
surfaces the stale-join warning instead of dropping it (issue #1945).
408+
409+
The component covers every type except ``tag``, so the merge falls to
410+
``_legacy_helper_list("tag")``; its registry read then fails, and the
411+
degrade-open warning must reach the merged response rather than vanish.
412+
"""
413+
covered = sorted(
414+
(tools_config_helpers.SIMPLE_HELPER_TYPES - {"tag"})
415+
| tools_config_helpers.FLOW_HELPER_TYPES
416+
)
417+
ws = make_ws(
418+
"ha_mcp_tools/helpers_list",
419+
info_result=_CAPS_HELPERS,
420+
cmd_result={"helpers": [], "count": 0, "covered_types": covered},
421+
)
422+
client = RegistryFailingClient()
423+
list_helpers = _build_list_helpers(client)
424+
425+
with patch_ws(ws, tools_config_helpers):
426+
resp = await list_helpers(helper_type="all")
427+
428+
assert resp["success"] is True
429+
assert tools_config_helpers._REGISTRY_JOIN_STALE_WARNING in resp.get("warnings", [])
430+
431+
381432
@pytest.mark.asyncio
382433
async def test_capsless_component_pins_legacy_path() -> None:
383434
"""Old component (info unknown_command) → legacy path, helpers_list never sent."""

0 commit comments

Comments
 (0)