One home for the user-reported regressions, all driven end to end against the
scoped stub harness:
- TestVoluptuousMirrorContract pins tests/voluptuous_mirror.py itself (a
default on the declaration is visible; a later re-declaration cannot add one)
so the harness cannot silently go blind again.
- TestDiscoveryPrefill: the discovered address is the default of BOTH host
keys, the port default is 5555, the step announces "Discovered Shield at
<host>", a manual/seed flow claims no discovery, and the error re-render
keeps the prefill. Reads the rendered schema, not the stored attribute.
- TestNoDeadEndPairing: skip protocol → entry created (passive), skip ADB →
entry created (ADB intent kept for the Pair ADB button) with the real
async_setup_entry accepting both entries (no ConfigEntryNotReady), skip ADB
routes on to the protocol step, both normal pair paths still create the
entry, a host typed on a pairing form is accepted in place, and a missing
androidtvremote2 reports protocol_unavailable instead of raising.
- TestCopyContract: no copy points at "the previous step", the no_host errors
name the field and the discovery prefill, every error/abort key raised by
config_flow.py exists in both copy files, and every field rendered by the
user/adb_pair/protocol_pair/options forms has a label.
- TestStringsTranslationsParity: strings.json and translations/en.json expose
the same key tree.
- TestPassiveSetupNeverDemandsReadiness: async_setup_entry raises nothing.
Verified: uvx --with pytest-asyncio pytest tests -q → 274 passed, 1 skipped;
uvx --with pytest-asyncio --with androidtvremote2 pytest tests -q → 275 passed;
uvx ruff check custom_components tests → clean.
Every user/pairing-step label and data_description rewritten in plain language,
and the fields the new pairing forms render now have labels at all (adb_host,
adb_port, skip_pairing, protocol_host, pairing_code).
No error string sends the user to "the previous step" any more: adb_no_host and
protocol_no_host name the field on the same form AND say a discovered Shield is
pre-filled automatically. The confirmation messages for both pairing steps
describe what the form actually does (including the restart-on-empty-submit
path), and protocol_unavailable covers a missing androidtvremote2.
The options-flow copy was audited in the same pass so it matches the setup
step's vocabulary ("Read state over ADB", "Show title and artist", ...), and
translations/en.json stays byte-identical to strings.json.
Verified: uvx --with pytest-asyncio pytest tests -q → 242 passed, 1 skipped;
uvx ruff check custom_components tests → clean.
protocol_pair and adb_pair now each offer "continue without pairing" (the new
CONF_SKIP_PAIRING form key), so the entry still finishes — in passive mode —
instead of stranding the user on a form with no way forward.
adb_pair: the ADB host/port live ON this form (the user step is gone), a host
typed here is authoritative, the addr the error names is therefore fixable
where the error appears, and skip routes through _async_after_pairing. ADB
stays enabled on purpose: the manager already degrades to its passive,
background-retrying state without a key, and the Pair ADB button (registered
for adb-enabled entries) then pairs later without re-adding the integration.
protocol_pair: the protocol host lives on this form too and a corrected
address restarts the pairing session against it; submitting without a code asks
the Shield for a fresh code (_async_restart_protocol_pairing) instead of
erroring; skip switches the transport off, because an unpaired protocol client
fails auth on its first connect and would only bounce the user into the reauth
flow — a fresh dead end. On reauth the skip aborts with reauth_skipped instead
of faking a repair.
_start_protocol_pairing also returns None now when build_protocol_client does
(androidtvremote2 missing) so the step reports protocol_unavailable instead of
crashing on a None client in the default CI variant.
Verified: uvx --with pytest-asyncio pytest tests -q → 242 passed, 1 skipped;
uvx ruff check custom_components tests → clean. The end-to-end skip-path and
copy tests land with the t33 UX suite commit.
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.