fix(config_flow): D3 — await the discovery-dedup coroutine (t37)

`ConfigFlow._async_handle_discovery_without_unique_id` is an `async def` in core
(verified: `inspect.iscoroutinefunction` is True) and core awaits it. t32 called
it without `await`, so the coroutine body never ran: no `already_configured`
abort when the handler already had entries, no
`DEFAULT_DISCOVERY_UNIQUE_ID` claim, no `already_in_progress` abort — HA's
discovery dedup was dead, Python warned "coroutine ... was never awaited", and
the host-equality loop that used to sit here was the only accidental dedup.

The call is awaited and the now-unreachable host loop is gone (the real helper
is stricter: any entry for the domain, or any in-flight flow). The harness
mirrors the real contract instead of a sync no-op:

* `entity_stublib`'s hub stub makes the helper a coroutine (a sync stub would
  turn the production `await` into a TypeError);
* `test_standalone_setup`'s stub models the real semantics — a coroutine raising
  `DiscoveryDedupAbortError` (core raises AbortFlow), driven by the shared
  holder's entry list and in-progress list — and its tests now assert all three
  outcomes: awaited-and-proceeds (counter + claimed discovery unique id),
  `already_configured` when entries exist, `already_in_progress` when another
  flow runs. A fourth test collects the un-awaited-coroutine warning, and the
  prefill test in the UX suite asserts the awaited counter.

Red proof: with the pre-fix config_flow.py the repo-owned sweep reports
`config_flow.py:425: missing-await: self._async_handle_discovery_without_unique_id()`.

Verified: 289 passed, 2 skipped (D3 stage); ruff clean.
This commit is contained in:
James Mackay committed 2026-10-10 21:20:13 +01:00
1 parent bd72ae3170
commit 60fb4f7557
4 files changed
+115 -34

No files matched your search

+11 -1
View File
@@ -114,7 +114,14 @@ def _install_stubs() -> None:
def _async_current_entries(self):
return []
def _async_handle_discovery_without_unique_id(self):
async def _async_handle_discovery_without_unique_id(self):
"""Real HA helper is a coroutine (t37 D3) — awaiting is mandatory.
This file never configures entries for the handler, so the real
aborts (already_configured / already_in_progress) cannot trigger;
tests/test_standalone_setup.py exercises those semantics.
"""
self._discovery_dedup_calls = getattr(self, "_discovery_dedup_calls", 0) + 1
return None
def _get_reauth_entry(self, entry=None):
@@ -496,6 +503,9 @@ class TestDiscoveryPrefill:
assert result["type"] == "form"
assert result["step_id"] == "user"
# t37 D3: MAC-less discovery needs the AWAITED coroutine helper — the
# counter only moves when the call is awaited
assert flow._discovery_dedup_calls == 1
defaults = vol_mirror.defaults_by_key(result["data_schema"])
# schema-level, not "the attribute was stored": this is precisely what
# the old assertion missed while the form rendered blank