fix(config_flow): F-A/F-A2/F-D — the discovered host and the 5555 port ride the schema KEY defaults (t33)

F-A (HIGH): _user_schema_dict re-declared vol.Optional(CONF_ADB_HOST) /
CONF_PROTOCOL_HOST inside `if "host" in defaults:` — a no-op, because
voluptuous keys compare by field name and the first (defaultless) key object
survives. The discovered address therefore never reached the form, and the
user submitted a blank host. The host keys are now declared WITH default=host
(_host_field_key), so a discovery-driven user step arrives pre-filled.

F-A2: a discovery-driven user step says the address is known
("{discovery_note}Discovered Shield at <host> — its address is already filled
in below. ") through description_placeholders, and the no_state_source
re-render keeps the prefill instead of rebuilding a blank schema.

F-D: the ADB port is declared with default=DEFAULT_ADB_PORT (5555), and a YAML
seed's port hint is carried into the form instead of being flattened to 5555.
The seed host now feeds _seed_host (same prefill, no discovery claim).

Reproduce-through-test: tests/voluptuous_mirror.py mirrors the real
voluptuous.Marker key contract (hash/eq by field name, default=UNDEFINED when
omitted), because voluptuous is not installed in either CI variant and the old
bare-key stubs could not tell a schema WITH a default from one without. The
pre-t33 discovery assertion only checked the stored attribute and stayed green
while the form rendered blank; it now reads the schema keys and fails on the
pre-fix code (8 tests reproduced red locally).

Verified: uvx --with pytest-asyncio pytest tests -q → 242 passed, 1 skipped;
uvx ruff check custom_components tests → clean.
This commit is contained in:
James Mackay committed 2026-10-10 20:50:16 +01:00
1 parent bbd98ebc3f
commit 73e886dafe
6 files changed
+193 -37

No files matched your search

+10 -4
View File
@@ -11,6 +11,8 @@ import enum
import sys
import types
import voluptuous_mirror
ENTITIES_STUB_NAMES = (
"voluptuous",
"homeassistant",
@@ -115,9 +117,9 @@ def install() -> None:
return
vol = types.ModuleType("voluptuous")
vol.Required = lambda key, default=None: key
vol.Optional = lambda key, default=None: key
vol.Schema = lambda schema: schema
# t33 F-A: the faithful mirror keeps schema defaults visible — a bare-key
# stub cannot tell a schema WITH a default from one without.
voluptuous_mirror.install(vol)
sys.modules["voluptuous"] = vol
ha = types.ModuleType("homeassistant")
@@ -148,12 +150,16 @@ def install() -> None:
# t32 F2 (audited helper): recording no-op at hub scope
pass
def async_show_form(self, *, step_id, data_schema=None, errors=None):
def async_show_form(
self, *, step_id, data_schema=None, errors=None,
description_placeholders=None,
):
return {
"type": "form",
"step_id": step_id,
"errors": dict(errors or {}),
"data_schema": data_schema,
"description_placeholders": dict(description_placeholders or {}),
}
def async_create_entry(self, *, title, data, options=None):
+21 -6
View File
@@ -15,6 +15,7 @@ from types import SimpleNamespace
from unittest import mock
import pytest
import voluptuous_mirror as vol_mirror
from conftest import _SHIELD_DIR, _load_module
from nsh.const import (
CONF_ADB_ENABLED,
@@ -54,9 +55,10 @@ def _install_stubs() -> None:
return
vol = types.ModuleType("voluptuous")
vol.Required = lambda key, default=None: key
vol.Optional = lambda key, default=None: key
vol.Schema = lambda schema: schema
# t33 F-A: a faithful mirror — the default stays ON the key, so a schema
# whose host default was dropped FAILS here instead of looking identical
# to one that carries it (the old bare-key stub hid exactly that bug).
vol_mirror.install(vol)
sys.modules["voluptuous"] = vol
ha = types.ModuleType("homeassistant")
@@ -71,12 +73,16 @@ def _install_stubs() -> None:
kwargs.pop("domain", None)
super().__init_subclass__(**kwargs)
def async_show_form(self, *, step_id, data_schema=None, errors=None):
def async_show_form(
self, *, step_id, data_schema=None, errors=None,
description_placeholders=None,
):
return {
"type": "form",
"step_id": step_id,
"errors": dict(errors or {}),
"data_schema": data_schema,
"description_placeholders": dict(description_placeholders or {}),
}
def async_create_entry(self, *, title, data, options=None):
@@ -942,6 +948,11 @@ class TestZeroconfDiscovery:
)
async def test_discovery_prefills_the_user_step(self, tmp_path):
"""t33 F-A: the discovered host is the DEFAULT OF THE SCHEMA KEYS.
The old check (the attribute was stored) stayed green while the
rendered form was blank — this one reads the schema itself.
"""
module = _form_module()
flow = module.NvidiaShieldConfigFlow()
flow.hass = FakeHass(str(tmp_path))
@@ -950,9 +961,13 @@ class TestZeroconfDiscovery:
assert result["type"] == "form"
assert result["step_id"] == "user"
# pre-fill landed: the discovered host is recorded for the schema
assert flow._discovered_host == "192.168.1.50"
assert result["data_schema"] is not None
defaults = vol_mirror.defaults_by_key(result["data_schema"])
assert defaults[CONF_ADB_HOST] == "192.168.1.50"
assert defaults[CONF_PROTOCOL_HOST] == "192.168.1.50"
# ...and a discovery-driven form says the address is already known
note = result["description_placeholders"]["discovery_note"]
assert "Discovered Shield at 192.168.1.50" in note
async def test_discovery_without_address_aborts(self, tmp_path):
module = _form_module()
+89
View File
@@ -0,0 +1,89 @@
"""A voluptuous mirror faithful enough to SEE a dropped schema default.
The test environment never installs voluptuous (the config-flow tests import
``config_flow`` against import-only stubs), so the harness has to model the two
behaviours the flow schemas depend on:
* ``Optional(field, default=value)`` / ``Required(field, default=value)`` carry
that default on the KEY object;
* keys compare by FIELD NAME only — real ``voluptuous.Marker`` implements
``__hash__ = hash(self.schema)`` and ``__eq__`` on class + schema. A later
re-declaration of the same field therefore keeps the FIRST key object, so a
default added by a second declaration is silently dropped.
That second property is exactly how the t33 F-A discovery-prefill bug hid from
the suite: the old stubs reduced every key to its bare string, so a schema
whose host key carried no default looked identical to one that carried the
discovered address. ``TestVoluptuousMirrorContract`` in
``tests/test_config_flow_ux.py`` pins both properties so the harness itself
cannot regress into blindness.
"""
from __future__ import annotations
UNDEFINED = object()
"""Sentinel for "this key declares no default" (real ``voluptuous.UNDEFINED``)."""
class Marker:
"""Stand-in for ``voluptuous.Marker`` (the ``Required``/``Optional`` base)."""
def __init__(self, schema, default=UNDEFINED):
self.schema = schema
self.default = default
def __hash__(self):
# real Marker hashes the field name only
return hash(self.schema)
def __eq__(self, other):
# real Marker compares class + field name, never the default
return type(other) is type(self) and other.schema == self.schema
def __repr__(self):
return f"<{type(self).__name__} {self.schema!r} default={self.default!r}>"
class Required(Marker):
"""Stand-in for ``voluptuous.Required``."""
class Optional(Marker):
"""Stand-in for ``voluptuous.Optional``."""
def install(vol) -> None:
"""Wire the mirror onto a stub ``voluptuous`` module object."""
vol.UNDEFINED = UNDEFINED
vol.Marker = Marker
vol.Required = Required
vol.Optional = Optional
vol.Schema = lambda schema: schema
def defaults_by_key(schema_dict) -> dict:
"""``{field: default}`` for a schema declared with mirror keys."""
return {key.schema: key.default for key in declared_keys(schema_dict)}
def declared_keys(schema_dict) -> list:
"""The schema's marker keys, in declaration order.
Raises on a non-marker key: a bare string key means the harness (or the
code under test) lost the KEY structure this mirror exists to check.
Test-harness bookkeeping keys (``__suggested__`` and friends) are skipped.
"""
keys = []
for key in schema_dict:
if isinstance(key, Marker):
keys.append(key)
continue
if isinstance(key, str) and key.startswith("__"):
continue
raise AssertionError(f"schema key {key!r} is not a voluptuous marker")
return keys
def declared_fields(schema_dict) -> list:
"""The field names of a schema, in declaration order."""
return [key.schema for key in declared_keys(schema_dict)]