P1: The registration was a silent no-op in production — it read
hass.data["lovelace"].dashboards as the DashboardsCollection and
awaited a sync async_items(), and passed "strategy"/"mode" keys the
real create schema rejects. Per the real lovelace source
(components/lovelace/__init__.py, _create_map_dashboard): the metadata
collection lives at hass.data["lovelace"]["dashboards_collection"],
async_items() is a SYNC @callback, the create payload carries metadata
only (url_path/title/icon/show_in_sidebar/allow_single_word), and the
dashboard CONFIG (the strategy reference) is saved as a SECOND step
through the content manager registered under
hass.data["lovelace"]["dashboards"][url_path]. The strategy is saved
only for a dashboard created in this run (or whose config already
carries ours); an existing dashboard skips both steps, so a user's
dashboard config is never overwritten. Collection unavailable / save
raising still warns and continues, never crashing setup.
P2: Declare "lovelace" in manifest.json dependencies (alongside
frontend/http) so hass.data["lovelace"] is populated before setup
instead of relying on boot order.
P3: Guard the check-then-create with a per-hass asyncio.Lock so two
concurrent entry setups cannot both pass the exists-check and race on
the url_path create. Per-hass (attribute on the hass object) rather
than a bare module-level lock, which binds to the first event loop
that awaits it.
The lifecycle tests' fake is rebuilt to mirror the real API shape
(sync async_items, async async_create_item, a dashboards content-
manager dict whose entries expose async async_save), with assertions
that the strategy config was saved with the custom:nestquest-party
type, that a user's existing config is not overwritten, that a failing
save still leaves setup green, and that concurrent registrations
create the dashboard exactly once (verified to fail without the
guard).