fix: enforce wall model barrier in backend optimize

Issue: #282
User-Visible: yes
This commit is contained in:
Matysh
2026-08-26 02:20:57 +03:00
parent fc9f513082
commit 71ac4a6639
4 changed files with 39 additions and 6 deletions
+26 -1
View File
@@ -60,6 +60,11 @@ from .virtual_lights import (
)
from .registry_snapshot import import_registry_snapshot
from .projection import project_config, project_layout
from .wall_segment_model import (
WALL_SEGMENT_MODEL_VERSION,
WallSegmentMigrationError,
commit_wall_segment_model,
)
from .validation import (
CONFIG_SCHEMA, LAYOUT_SCHEMA, MAX_CONFIG_BYTES, MAX_PLAN_BYTES,
PLAN_EXTENSIONS, POS_SCHEMA, MarkerControlError, OpeningPassageError,
@@ -1633,7 +1638,26 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
# as config/set; otherwise a crafted client can persist a new cycle.
try:
validate_wall_model_transition(msg["config"], config_data.get("config"))
validated_config = CONFIG_SCHEMA(msg["config"])
try:
submitted_model = int(msg["config"].get("model_version", 0) or 0)
except (TypeError, ValueError):
submitted_model = WALL_SEGMENT_MODEL_VERSION
# Optimize is an explicit structural writer and therefore the
# server-side v7 -> v8 barrier as well. Current v8 candidates are
# validated verbatim: the backend must never invent lineage for a
# graph already authored by a current client.
candidate_config = CONFIG_SCHEMA(msg["config"])
if submitted_model < WALL_SEGMENT_MODEL_VERSION:
candidate_config, _ = commit_wall_segment_model(candidate_config)
validated_config = CONFIG_SCHEMA(candidate_config)
migrated_size = len(json.dumps(validated_config, separators=(",", ":")))
if migrated_size > MAX_CONFIG_BYTES:
connection.send_error(
msg["id"], "too_large",
f"Configuration is {migrated_size // 1024} KB, "
f"the limit is {MAX_CONFIG_BYTES // 1024} KB",
)
return
msg["config"].clear()
msg["config"].update(validated_config)
validate_marker_controls(msg["config"], config_data.get("config"))
@@ -1647,6 +1671,7 @@ async def ws_plan_optimize(hass: HomeAssistant, connection, msg: dict[str, Any])
except (
MarkerControlError, OpeningPassageError, PartitionOpeningHostError,
PartitionOpeningJambMarginError, WallModelClientOutdatedError,
WallSegmentMigrationError,
) as err:
connection.send_error(msg["id"], err.code, str(err))
return
+3 -2
View File
@@ -18,8 +18,9 @@
passage ownership attached to stable wall-segment IDs. Existing plans upgrade
atomically on their first structural edit or through **Optimize plans**;
Resize, Undo/Redo and repeated edits preserve the same identities without
accumulating off-grid coordinates. Ambiguous legacy geometry is left
unchanged with a recovery prompt. No Plan editor controls or
accumulating off-grid coordinates. The backend Optimize transaction also
enforces this upgrade when invoked directly. Ambiguous legacy geometry is
left unchanged with a recovery prompt. No Plan editor controls or
zero-thickness-wall behaviour changed
([#282](https://github.com/Matysh/houseplan-card/issues/282)).
- The active wall chain keeps its axis and node markers visible on already
+3 -1
View File
@@ -22,7 +22,9 @@
сегментов и принадлежность дверей, окон, ворот и проходов через стабильные ID
сегментов стен. Существующий план обновляется атомарно при первом структурном
изменении или через **«Оптимизировать планы»**; Resize, отмена/повтор и
последующие правки сохраняют те же ID без накопления координат вне сетки.
последующие правки сохраняют те же ID без накопления координат вне сетки;
серверная транзакция Optimize также гарантирует это обновление при прямом
вызове.
Неоднозначная старая геометрия остаётся без изменений с подсказкой по
восстановлению. Кнопки редактора плана и поведение стен нулевой толщины не
менялись
+7 -2
View File
@@ -568,8 +568,13 @@ async def test_optimize_accepts_proved_rehost_and_undo_restores_host(
optimized = await client.receive_json()
assert optimized["success"]
stored = (await runtime.config_store.async_load())["config"]
assert stored["spaces"][0].get("partitions") in (None, [])
assert "host" not in stored["spaces"][0]["openings"][0]
stored_space = stored["spaces"][0]
assert stored_space.get("partitions") in (None, [])
wall_ids = {segment["id"] for segment in stored_space["wall_segments"]}
assert wall_ids
for opening in stored_space["openings"]:
assert opening["host"]["kind"] == "wall"
assert opening["host"]["id"] in wall_ids
await client.send_json_auto_id({
"type": "houseplan/plan/optimize_undo",