|
13 | 13 |
|
14 | 14 | from __future__ import annotations |
15 | 15 |
|
| 16 | +import asyncio |
16 | 17 | import logging |
17 | 18 | import uuid |
18 | 19 | from typing import Any |
@@ -49,6 +50,18 @@ def _backups_for( |
49 | 50 | return [e for e in entries if e["domain"] == domain and e["entity_id"] == entity_id] |
50 | 51 |
|
51 | 52 |
|
| 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 | + |
52 | 65 | async def _wait_for_backup( |
53 | 66 | mcp_client, *, domain: str, entity_id: str, timeout: int = 15 |
54 | 67 | ) -> str: |
@@ -267,6 +280,9 @@ async def test_input_boolean_full_loop( |
267 | 280 | }, |
268 | 281 | ) |
269 | 282 | 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) |
270 | 286 | # Edit so capture fires (the create call may or may not capture |
271 | 287 | # depending on whether helper_id is None — edit definitely does). |
272 | 288 | edit = await safe_call_tool( |
@@ -354,6 +370,9 @@ async def test_schedule_full_loop( |
354 | 370 | } |
355 | 371 | create = await safe_call_tool(mcp_client, "ha_config_set_helper", original) |
356 | 372 | 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) |
357 | 376 |
|
358 | 377 | # Edit — shrink Monday, split Tuesday, drop Wednesday entirely. |
359 | 378 | # ``name`` is required by HA's schedule schema on every update |
@@ -451,6 +470,8 @@ async def test_dashboard_full_loop( |
451 | 470 | ) |
452 | 471 | if create.get("success") is False: |
453 | 472 | 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) |
454 | 475 | # Edit so the decorator captures the pre-edit state. |
455 | 476 | await safe_call_tool( |
456 | 477 | mcp_client, |
@@ -517,6 +538,8 @@ async def test_script_full_loop( |
517 | 538 | ) |
518 | 539 | if create.get("success") is False: |
519 | 540 | 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) |
520 | 543 | # Edit triggers capture. |
521 | 544 | await safe_call_tool( |
522 | 545 | mcp_client, |
@@ -583,6 +606,8 @@ async def test_scene_full_loop( |
583 | 606 | ) |
584 | 607 | if create.get("success") is False: |
585 | 608 | 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) |
586 | 611 | await safe_call_tool( |
587 | 612 | mcp_client, |
588 | 613 | "ha_config_set_scene", |
@@ -716,6 +741,8 @@ async def test_label_full_loop( |
716 | 741 | pytest.skip(f"label create unsupported: {create}") |
717 | 742 | label_id = create.get("data", {}).get("label_id") or create.get("label_id") |
718 | 743 | 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) |
719 | 746 |
|
720 | 747 | # Edit — change name + color so capture fires. |
721 | 748 | edit = await safe_call_tool( |
@@ -787,6 +814,8 @@ async def test_category_full_loop( |
787 | 814 | cat_id = create.get("data", {}).get("category_id") or create.get("category_id") |
788 | 815 | assert cat_id, f"category_id missing: {create}" |
789 | 816 | 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) |
790 | 819 |
|
791 | 820 | edit = await safe_call_tool( |
792 | 821 | mcp_client, |
@@ -859,6 +888,8 @@ async def test_zone_full_loop( |
859 | 888 | pytest.skip(f"zone create unsupported: {create}") |
860 | 889 | zone_id = create.get("data", {}).get("zone_id") or create.get("zone_id") |
861 | 890 | 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) |
862 | 893 |
|
863 | 894 | edit = await safe_call_tool( |
864 | 895 | mcp_client, |
@@ -926,6 +957,8 @@ async def test_area_full_loop( |
926 | 957 | area_id = create.get("data", {}).get("area_id") or create.get("area_id") |
927 | 958 | assert area_id, f"area_id missing: {create}" |
928 | 959 | 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) |
929 | 962 |
|
930 | 963 | edit = await safe_call_tool( |
931 | 964 | mcp_client, |
@@ -995,6 +1028,8 @@ async def test_group_full_loop( |
995 | 1028 | ) |
996 | 1029 | if create.get("success") is False: |
997 | 1030 | 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) |
998 | 1033 |
|
999 | 1034 | edit = await safe_call_tool( |
1000 | 1035 | mcp_client, |
@@ -1132,6 +1167,8 @@ async def test_dashboard_resource_full_loop( |
1132 | 1167 | "resource_id" |
1133 | 1168 | ) |
1134 | 1169 | 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) |
1135 | 1172 |
|
1136 | 1173 | # Edit — change the URL so capture fires. |
1137 | 1174 | edit = await safe_call_tool( |
|
0 commit comments