Skip to content

Commit df50de1

Browse files
fix: handle null trigger states in automation traces (#2203)
* fix: handle null trigger states in automation traces * test: strengthen automation trace regression coverage --------- Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
1 parent 1e69b77 commit df50de1

3 files changed

Lines changed: 124 additions & 5 deletions

File tree

src/ha_mcp/tools/tools_traces.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,13 +782,13 @@ def _populate_trigger_info(
782782
"description": trigger_vars.get("description"),
783783
}
784784
if "to_state" in trigger_vars:
785-
result["trigger"]["to_state"] = trigger_vars.get("to_state", {}).get(
785+
result["trigger"]["to_state"] = (trigger_vars.get("to_state") or {}).get(
786786
"state"
787787
)
788788
if "from_state" in trigger_vars:
789-
result["trigger"]["from_state"] = trigger_vars.get("from_state", {}).get(
790-
"state"
791-
)
789+
result["trigger"]["from_state"] = (
790+
trigger_vars.get("from_state") or {}
791+
).get("state")
792792
if "entity_id" in trigger_vars:
793793
result["trigger"]["entity_id"] = trigger_vars["entity_id"]
794794
if "error" in trigger_step:

tests/src/e2e/workflows/automation/test_traces.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@
66
"""
77

88
import logging
9+
import time
910
from typing import Any
1011

1112
import pytest
1213

1314
from ...utilities.assertions import (
15+
MCPAssertions,
1416
assert_mcp_success,
1517
parse_mcp_result,
1618
safe_call_tool,
1719
)
1820
from ...utilities.entity_finders import find_test_light_entity
19-
from ...utilities.wait_helpers import wait_for_condition
21+
from ...utilities.wait_helpers import wait_for_condition, wait_for_tool_result
2022

2123
logger = logging.getLogger(__name__)
2224

@@ -30,6 +32,67 @@ async def _find_test_light_entity(self, mcp_client) -> str:
3032
"""Delegates to the suite-wide helper in utilities.entity_finders."""
3133
return await find_test_light_entity(mcp_client)
3234

35+
async def test_template_trace_with_null_trigger_states(
36+
self,
37+
mcp_client: Any,
38+
test_data_factory: Any,
39+
) -> None:
40+
"""Return time-driven template traces whose HA states are null."""
41+
trigger_after = int(time.time()) + 45
42+
create_config = test_data_factory.automation_config(
43+
"Template Null State Trace",
44+
trigger=[
45+
{
46+
"platform": "template",
47+
"value_template": (
48+
f"{{{{ as_timestamp(now()) >= {trigger_after} }}}}"
49+
),
50+
}
51+
],
52+
action=[{"delay": {"seconds": 0}}],
53+
initial_state=True,
54+
)
55+
56+
automation_id: str | None = None
57+
try:
58+
async with MCPAssertions(mcp_client) as mcp:
59+
create_data = await mcp.call_tool_success(
60+
"ha_config_set_automation", {"config": create_config}
61+
)
62+
automation_id = create_data.get("entity_id") or create_data.get(
63+
"automation_id"
64+
)
65+
assert automation_id is not None
66+
67+
traces_data = await wait_for_tool_result(
68+
mcp_client,
69+
tool_name="ha_get_automation_traces",
70+
arguments={"automation_id": automation_id},
71+
predicate=lambda data: data.get("trace_count", 0) > 0,
72+
timeout=120,
73+
poll_interval=1,
74+
description="time-driven template automation trace",
75+
)
76+
run_id = traces_data["traces"][0]["run_id"]
77+
detailed_data = await mcp.call_tool_success(
78+
"ha_get_automation_traces",
79+
{"automation_id": automation_id, "run_id": run_id},
80+
)
81+
82+
assert detailed_data["trigger"]["platform"] == "template"
83+
assert detailed_data["trigger"]["from_state"] is None
84+
assert detailed_data["trigger"]["to_state"] is None
85+
finally:
86+
if automation_id is not None:
87+
cleanup_data = await safe_call_tool(
88+
mcp_client,
89+
"ha_config_remove_automation",
90+
{"identifier": automation_id},
91+
)
92+
assert cleanup_data.get("success") is True, (
93+
f"Failed to remove test automation: {cleanup_data}"
94+
)
95+
3396
async def test_automation_trace_after_trigger(
3497
self, mcp_client, cleanup_tracker, test_data_factory
3598
):

tests/src/unit/test_tools_traces_detail.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,62 @@ def test_format_flat_trace_structure(self):
8080
assert actions[0]["result"]["params"]["service"] == "turn_on"
8181
assert actions[1]["child_id"]["item_id"] == "set_brightness_chambre"
8282

83+
def test_format_template_trigger_with_null_states(self) -> None:
84+
"""Preserve null states from time-driven template trigger traces."""
85+
trace_data = {
86+
"state": "stopped",
87+
"trace": {
88+
"trigger/0": [
89+
{
90+
"path": "trigger/0",
91+
"changed_variables": {
92+
"trigger": {
93+
"platform": "template",
94+
"description": "time change or manual update via template",
95+
"entity_id": None,
96+
"from_state": None,
97+
"to_state": None,
98+
}
99+
},
100+
}
101+
]
102+
},
103+
}
104+
105+
result = _format_detailed_trace("automation.test", "run_123", trace_data)
106+
107+
assert result["success"] is True
108+
assert result["trigger"] == {
109+
"platform": "template",
110+
"description": "time change or manual update via template",
111+
"entity_id": None,
112+
"from_state": None,
113+
"to_state": None,
114+
}
115+
116+
def test_format_template_trigger_with_state_objects(self) -> None:
117+
"""Preserve state strings from entity-driven template trigger traces."""
118+
trace_data = {
119+
"trace": {
120+
"trigger/0": [
121+
{
122+
"changed_variables": {
123+
"trigger": {
124+
"platform": "template",
125+
"from_state": {"state": "off"},
126+
"to_state": {"state": "on"},
127+
}
128+
}
129+
}
130+
]
131+
}
132+
}
133+
134+
result = _format_detailed_trace("automation.test", "run_123", trace_data)
135+
136+
assert result["trigger"]["from_state"] == "off"
137+
assert result["trigger"]["to_state"] == "on"
138+
83139
def test_format_legacy_trace_structure(self):
84140
"""Test fallback parsing of potential legacy trace structure (lists)."""
85141

0 commit comments

Comments
 (0)