Skip to content

Commit 2c07c06

Browse files
kingpanther13claude
andcommitted
fix(flows): scope nested step_values leaves out of reuse too
step_scoped holds the overlay's TOP-LEVEL keys, but the guard compared the leaf name — so an overlay key naming a section let every value inside it through. For {'connection': {'province': 'TX'}} the leaf is 'province', which is nowhere in {'connection'}, and a later step declaring connection.province resubmitted TX instead of its stored value. Matched on the root of the declaration path instead. Found by CodeRabbit review on #2256. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012d29UJTiH4Uy2Pm37SBPtr
1 parent 4e1049e commit 2c07c06

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/ha_mcp/tools/config_entry_flow_form.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,17 @@ def record(
123123
"""
124124
dotted = _section_path(path_prefix, name)
125125
self.filled.add(dotted)
126-
if name in self.step_scoped:
126+
if dotted.split(".", 1)[0] in self.step_scoped:
127127
# Supplied by ``step_values`` for THIS step only. ``filled`` still
128128
# takes it so nothing is injected over it here, but it must never
129129
# reach ``flat``/``scoped``: those survive the whole walk, and a
130130
# later step that nobody addressed would then reuse a value the
131131
# caller scoped to one step instead of its own stored one.
132+
#
133+
# Matched on the ROOT of the declaration path, not the leaf name:
134+
# an overlay key naming a SECTION carries leaves whose own names
135+
# are nowhere in ``step_scoped``, so comparing the leaf let every
136+
# nested value through (CodeRabbit review, issue #2254).
132137
return
133138
if scoped_only:
134139
self.scoped[dotted] = copy.deepcopy(value)

tests/src/unit/test_flow_options_preserve.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,67 @@ def test_a_step_scoped_value_never_leaks_to_an_unaddressed_step(self) -> None:
961961
f"The step-scoped value leaked into an unaddressed step: {seen[2]}"
962962
)
963963

964+
def test_a_nested_step_scoped_value_never_leaks_either(self) -> None:
965+
"""An overlay key naming a SECTION scopes its leaves too.
966+
967+
``step_scoped`` holds the overlay's top-level keys, so matching the
968+
leaf name let every value inside a section through: for
969+
``{"connection": {"province": "TX"}}`` the leaf is ``province``, which
970+
is nowhere in ``{"connection"}``. The guard matches the ROOT of the
971+
declaration path instead (CodeRabbit review, #2256).
972+
"""
973+
974+
def step(step_id: str) -> dict[str, Any]:
975+
return {
976+
"type": "form",
977+
"step_id": step_id,
978+
"data_schema": [
979+
{
980+
"type": "expandable",
981+
"name": "connection",
982+
"required": False,
983+
"schema": [
984+
{
985+
"name": "province",
986+
"required": False,
987+
"optional": True,
988+
"description": {"suggested_value": "BW"},
989+
},
990+
],
991+
},
992+
],
993+
}
994+
995+
reuse_state = _ReuseState()
996+
remaining: dict[str, Any] = {
997+
"step_values": {"one": {"connection": {"province": "TX"}}}
998+
}
999+
first = _handle_form_step(
1000+
"flow-2254",
1001+
step("one"),
1002+
remaining,
1003+
None,
1004+
set(),
1005+
reuse_state,
1006+
keep_current_values=True,
1007+
)
1008+
second = _handle_form_step(
1009+
"flow-2254",
1010+
step("two"),
1011+
remaining,
1012+
None,
1013+
set(),
1014+
reuse_state,
1015+
keep_current_values=True,
1016+
)
1017+
1018+
assert first == {"connection": {"province": "TX"}}
1019+
assert second == {"connection": {"province": "BW"}}, (
1020+
f"The nested step-scoped value leaked: {second}"
1021+
)
1022+
assert reuse_state.scoped == {}
1023+
assert reuse_state.flat == {}
1024+
9641025
def test_an_undeclared_field_inside_step_values_is_reported(self) -> None:
9651026
"""A typo'd FIELD inside an entry must not vanish silently.
9661027

0 commit comments

Comments
 (0)