HP-1470-02: only refuse a plan reference that is NEW and already broken

CI caught what the local pure suite cannot run. Four HA-harness tests store a
plan url whose file is not there — and so, sooner or later, will a user: files
disappear from outside Home Assistant, and one of them is what the 'broken plan'
repair exists to report. Refusing every write that names a missing file would
have locked the owner out of every edit, including detaching it.

So the check compares against the stored configuration and only refuses names it
has not seen before, which is exactly the pick-then-delete window it was written
for. The repairs test now attaches a real plan and removes the file behind it;
the quota test budgets from what the shared test config directory already holds
instead of assuming an empty folder.
This commit is contained in:
Matysh
2026-07-28 22:51:07 +03:00
parent f5e6c0318d
commit c00048611e
2 changed files with 66 additions and 10 deletions
+30 -7
View File
@@ -477,12 +477,11 @@ async def ws_config_get(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
def _missing_internal_plans(plans_dir: Path, config: dict[str, Any]) -> set[str]:
"""Plan files a configuration names that are not on disk.
def _internal_plan_names(config: dict[str, Any]) -> set[str]:
"""Plan file names a configuration names through OUR urls.
Only OUR urls are checked — `/api/houseplan/content/plans/_/<name>` and the
legacy static path. Anything else belongs to the user and may point wherever
they like.
Only `/api/houseplan/content/plans/_/<name>` and the legacy static path
count. Anything else belongs to the user and may point wherever they like.
"""
out: set[str] = set()
for space in (config or {}).get("spaces") or []:
@@ -492,11 +491,32 @@ def _missing_internal_plans(plans_dir: Path, config: dict[str, Any]) -> set[str]
if not (url.startswith(CONTENT_URL + "/plans/") or url.startswith(PLANS_URL + "/")):
continue
name = plan_basename(url)
if name and not (plans_dir / name).is_file():
if name:
out.add(name)
return out
def _missing_internal_plans(
plans_dir: Path, config: dict[str, Any], previous: dict[str, Any] | None = None
) -> set[str]:
"""Newly named plan files that are not on disk.
Guards the pick-then-save window: another client may delete a plan between
the moment this one chose it and the moment it saves, which would otherwise
store a url with nothing behind it (HP-1470-02).
A name the stored configuration already carries is deliberately let through.
It is already broken — repairs says so — and refusing the write would lock
the owner out of every other edit, including the one that detaches it.
"""
known = _internal_plan_names(previous or {})
return {
name
for name in _internal_plan_names(config)
if name not in known and not (plans_dir / name).is_file()
}
@websocket_api.websocket_command(
{
vol.Required("type"): "houseplan/config/set",
@@ -552,7 +572,10 @@ async def ws_config_set(hass: HomeAssistant, connection, msg: dict[str, Any]) ->
# nothing about whether the file survived (HP-1470-02). External and
# legacy urls are not ours to check and are left alone.
missing = await hass.async_add_executor_job(
_missing_internal_plans, Path(hass.config.path(PLANS_DIR)), msg["config"]
_missing_internal_plans,
Path(hass.config.path(PLANS_DIR)),
msg["config"],
data.get("config"),
)
if missing:
connection.send_error(