Skip to content

Commit 402295f

Browse files
committed
fix(backup): zone WS commands + e2e settle delay before edit
Two issues exposed by the new e2e full-loop tests: 1. ``_fetch_zone`` and ``_restore_zone`` were calling ``config/zone/list`` and ``config/zone/update`` WS commands. HA's actual zone API has no ``config/`` prefix — confirmed against ``tools_zones.py:71,280,296`` which use the bare ``zone/list``, ``zone/update``, ``zone/create``. The wrong commands surfaced as ``HomeAssistantCommandError: Command failed: Unknown command`` in the auto-backup WARNING log on every zone edit, with capture silently skipped. Real PR bug, not just a test bug. 2. The new full-loop tests (helper, label, category, zone, area, group, dashboard_resource) did ``create → edit → expect capture`` back-to-back. HA's WS-backed registries don't always have the freshly-created entity in the ``<x>/list`` response by the time the edit's decorator pre-fetch fires — fetch returns None, snapshot is skipped, and polling the backup file afterward can't recover. Add a fixed 2s ``await asyncio.sleep`` between create and edit so the registry has time to index the new entity. Automation's REST upsert path settles faster and doesn't need this (its existing ``test_full_loop`` was the only one passing). Schedule's view-the-captured-config assertion still asserts on the original nested structure since the snapshot is taken pre-edit.
1 parent 53ec392 commit 402295f

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

src/ha_mcp/backup_manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -870,11 +870,12 @@ async def _restore_calendar_event(client: Any, entity_id: str, config: Any) -> A
870870
)
871871

872872

873-
# Zones — config/zone/{list,update}
873+
# Zones — zone/{list,update} (no ``config/`` prefix per HA's actual WS API;
874+
# matches ``tools_zones.py`` which is the authoritative usage).
874875

875876

876877
async def _fetch_zone(client: Any, entity_id: str) -> Any:
877-
items = await _ws_send(client, {"type": "config/zone/list"})
878+
items = await _ws_send(client, {"type": "zone/list"})
878879
if not isinstance(items, list):
879880
return None
880881
for item in items:
@@ -885,7 +886,7 @@ async def _fetch_zone(client: Any, entity_id: str) -> Any:
885886

886887
async def _restore_zone(client: Any, entity_id: str, config: Any) -> Any:
887888
payload = {k: v for k, v in config.items() if k != "id"}
888-
payload["type"] = "config/zone/update"
889+
payload["type"] = "zone/update"
889890
payload["zone_id"] = config.get("id", entity_id)
890891
return await _ws_send(client, payload)
891892

tests/src/e2e/workflows/auto_backup/test_capture_and_restore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from __future__ import annotations
1515

16+
import asyncio
1617
import logging
1718
import uuid
1819
from typing import Any
@@ -49,6 +50,18 @@ def _backups_for(
4950
return [e for e in entries if e["domain"] == domain and e["entity_id"] == entity_id]
5051

5152

53+
# Fixed delay between create and edit to let HA's WS-backed registries
54+
# index the freshly-created entity before the decorator's pre-edit
55+
# fetch fires. The decorator is best-effort: if fetch returns None
56+
# (entity not in the registry list yet), the snapshot is silently
57+
# skipped — and polling the backup file afterwards can't recover that.
58+
# 2 s is the empirically-stable mark on HAOS testcontainer runners;
59+
# automation's REST upsert path settles faster and doesn't need this,
60+
# but every WS-backed domain (label, category, zone, area, helper,
61+
# dashboard_resource) does.
62+
_HA_PROPAGATION_SETTLE_SECONDS = 2.0
63+
64+
5265
async def _wait_for_backup(
5366
mcp_client, *, domain: str, entity_id: str, timeout: int = 15
5467
) -> str:
@@ -267,6 +280,9 @@ async def test_input_boolean_full_loop(
267280
},
268281
)
269282
assert create.get("success") is not False
283+
# Let HA index the new helper before the edit fires; otherwise the
284+
# decorator's pre-edit fetch via ``input_boolean/list`` may miss it.
285+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
270286
# Edit so capture fires (the create call may or may not capture
271287
# depending on whether helper_id is None — edit definitely does).
272288
edit = await safe_call_tool(
@@ -354,6 +370,9 @@ async def test_schedule_full_loop(
354370
}
355371
create = await safe_call_tool(mcp_client, "ha_config_set_helper", original)
356372
assert create.get("success") is not False
373+
# Let HA index the new helper before the edit fires; the decorator's
374+
# pre-edit fetch via ``schedule/list`` needs the entity present.
375+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
357376

358377
# Edit — shrink Monday, split Tuesday, drop Wednesday entirely.
359378
# ``name`` is required by HA's schedule schema on every update
@@ -451,6 +470,8 @@ async def test_dashboard_full_loop(
451470
)
452471
if create.get("success") is False:
453472
pytest.skip(f"dashboard create unsupported on this HA: {create}")
473+
# Let HA settle the new lovelace config before the edit fires.
474+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
454475
# Edit so the decorator captures the pre-edit state.
455476
await safe_call_tool(
456477
mcp_client,
@@ -517,6 +538,8 @@ async def test_script_full_loop(
517538
)
518539
if create.get("success") is False:
519540
pytest.skip(f"script create unsupported: {create}")
541+
# Settle so the decorator's pre-edit fetch finds the script.
542+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
520543
# Edit triggers capture.
521544
await safe_call_tool(
522545
mcp_client,
@@ -583,6 +606,8 @@ async def test_scene_full_loop(
583606
)
584607
if create.get("success") is False:
585608
pytest.skip(f"scene create unsupported: {create}")
609+
# Settle so the decorator's pre-edit fetch finds the scene.
610+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
586611
await safe_call_tool(
587612
mcp_client,
588613
"ha_config_set_scene",
@@ -716,6 +741,8 @@ async def test_label_full_loop(
716741
pytest.skip(f"label create unsupported: {create}")
717742
label_id = create.get("data", {}).get("label_id") or create.get("label_id")
718743
assert label_id, f"label_id missing from create response: {create}"
744+
# Settle so the decorator's pre-edit fetch finds the label.
745+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
719746

720747
# Edit — change name + color so capture fires.
721748
edit = await safe_call_tool(
@@ -787,6 +814,8 @@ async def test_category_full_loop(
787814
cat_id = create.get("data", {}).get("category_id") or create.get("category_id")
788815
assert cat_id, f"category_id missing: {create}"
789816
composite = f"{scope}:{cat_id}"
817+
# Settle so the decorator's pre-edit fetch finds the category.
818+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
790819

791820
edit = await safe_call_tool(
792821
mcp_client,
@@ -859,6 +888,8 @@ async def test_zone_full_loop(
859888
pytest.skip(f"zone create unsupported: {create}")
860889
zone_id = create.get("data", {}).get("zone_id") or create.get("zone_id")
861890
assert zone_id, f"zone_id missing: {create}"
891+
# Settle so the decorator's pre-edit fetch finds the zone.
892+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
862893

863894
edit = await safe_call_tool(
864895
mcp_client,
@@ -926,6 +957,8 @@ async def test_area_full_loop(
926957
area_id = create.get("data", {}).get("area_id") or create.get("area_id")
927958
assert area_id, f"area_id missing: {create}"
928959
composite = f"area:{area_id}"
960+
# Settle so the decorator's pre-edit fetch finds the area.
961+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
929962

930963
edit = await safe_call_tool(
931964
mcp_client,
@@ -995,6 +1028,8 @@ async def test_group_full_loop(
9951028
)
9961029
if create.get("success") is False:
9971030
pytest.skip(f"group create unsupported: {create}")
1031+
# Settle so the decorator's pre-edit fetch finds the group state.
1032+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
9981033

9991034
edit = await safe_call_tool(
10001035
mcp_client,
@@ -1132,6 +1167,8 @@ async def test_dashboard_resource_full_loop(
11321167
"resource_id"
11331168
)
11341169
assert resource_id, f"resource_id missing: {create}"
1170+
# Settle so the decorator's pre-edit fetch finds the resource.
1171+
await asyncio.sleep(_HA_PROPAGATION_SETTLE_SECONDS)
11351172

11361173
# Edit — change the URL so capture fires.
11371174
edit = await safe_call_tool(

0 commit comments

Comments
 (0)