mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
fix: keep Optimize idempotent across storage reload
Issue: #248 User-Visible: yes
This commit is contained in:
@@ -33,6 +33,12 @@ FIXTURE = (
|
||||
/ "fixtures"
|
||||
/ "coordinate-canonicalization.json"
|
||||
)
|
||||
OPTIMIZE_ROUNDTRIP_FIXTURE = (
|
||||
Path(__file__).parents[1]
|
||||
/ "test"
|
||||
/ "fixtures"
|
||||
/ "optimize-storage-roundtrip.json"
|
||||
)
|
||||
|
||||
|
||||
def _fixture() -> dict:
|
||||
@@ -76,6 +82,17 @@ def test_backend_schemas_apply_the_same_allowlist() -> None:
|
||||
assert POS_SCHEMA(fixture["layoutInput"]["virtual"]) == fixture["layoutExpected"]["virtual"]
|
||||
|
||||
|
||||
def test_optimize_roundtrip_fixture_has_one_backend_canonical_target() -> None:
|
||||
"""#248: Python consumes the same cross-runtime target as the Node test."""
|
||||
fixture = json.loads(OPTIMIZE_ROUNDTRIP_FIXTURE.read_text(encoding="utf-8"))
|
||||
source = fixture["input"]
|
||||
expected = fixture["expected"]
|
||||
assert canonicalize_config_geometry(source["config"]) == expected["config"]
|
||||
assert canonicalize_layout_geometry(source["layout"]) == expected["layout"]
|
||||
assert CONFIG_SCHEMA(source["config"]) == expected["config"]
|
||||
assert LAYOUT_SCHEMA(source["layout"]) == expected["layout"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_storage_helpers_are_the_final_canonical_barrier(monkeypatch) -> None:
|
||||
fixture = _fixture()
|
||||
|
||||
@@ -1627,6 +1627,49 @@ async def test_setup_recovers_durable_import_pair(
|
||||
assert "optimize_pending" not in layout_data
|
||||
|
||||
|
||||
async def test_setup_recovers_exact_optimize_storage_roundtrip_pair(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""#248: startup recovery converges on the same shared canonical target."""
|
||||
fixture = json.loads((
|
||||
Path(__file__).parents[1]
|
||||
/ "test"
|
||||
/ "fixtures"
|
||||
/ "optimize-storage-roundtrip.json"
|
||||
).read_text(encoding="utf-8"))
|
||||
expected = fixture["expected"]
|
||||
|
||||
entry = await _setup(hass)
|
||||
rt = get_data(hass)
|
||||
assert rt is not None
|
||||
await rt.config_store.async_save({"config": DEFAULT_CONFIG, "rev": 1})
|
||||
await rt.store.async_save({
|
||||
"layout": {},
|
||||
"rev": 1,
|
||||
"optimize_backup": {"sentinel": True},
|
||||
"optimize_pending": {
|
||||
"kind": "optimize",
|
||||
"config": expected["config"],
|
||||
"layout": expected["layout"],
|
||||
"config_rev": 2,
|
||||
"layout_rev": 2,
|
||||
"clear_backup": False,
|
||||
},
|
||||
})
|
||||
|
||||
assert await hass.config_entries.async_reload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
recovered = get_data(hass)
|
||||
assert recovered is not None
|
||||
config_data = await recovered.config_store.async_load()
|
||||
layout_data = await recovered.store.async_load()
|
||||
assert config_data == {"config": expected["config"], "rev": 2}
|
||||
assert layout_data["layout"] == expected["layout"]
|
||||
assert layout_data["rev"] == 2
|
||||
assert layout_data["optimize_backup"] == {"sentinel": True}
|
||||
assert "optimize_pending" not in layout_data
|
||||
|
||||
|
||||
async def test_apply_rechecks_plan_file_under_the_write_lock(
|
||||
hass: HomeAssistant, tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""WebSocket API tests (CI): layout ops, config rev conflict, not_ready gate."""
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -530,6 +534,64 @@ async def test_optimize_undo_restores_geometry_but_not_legacy_noisy_bits(
|
||||
assert restored_layout["lamp"]["x"] == -0.12345679
|
||||
|
||||
|
||||
async def test_plan_optimize_persists_exact_storage_roundtrip_target(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, monkeypatch
|
||||
) -> None:
|
||||
"""#248: intent, normal commit and reads expose one canonical pair."""
|
||||
from custom_components.houseplan.store import get_data
|
||||
|
||||
fixture = json.loads((
|
||||
Path(__file__).parents[1]
|
||||
/ "test"
|
||||
/ "fixtures"
|
||||
/ "optimize-storage-roundtrip.json"
|
||||
).read_text(encoding="utf-8"))
|
||||
source = fixture["input"]
|
||||
expected = fixture["expected"]
|
||||
|
||||
await _setup(hass)
|
||||
client = await hass_ws_client(hass)
|
||||
runtime = get_data(hass)
|
||||
assert runtime is not None
|
||||
|
||||
layout_writes = []
|
||||
real_layout_save = runtime.store.async_save
|
||||
|
||||
async def capture_layout_write(value: dict) -> None:
|
||||
layout_writes.append(copy.deepcopy(value))
|
||||
await real_layout_save(value)
|
||||
|
||||
monkeypatch.setattr(runtime.store, "async_save", capture_layout_write)
|
||||
await client.send_json_auto_id({
|
||||
"type": "houseplan/plan/optimize",
|
||||
"config": source["config"],
|
||||
"layout": source["layout"],
|
||||
"expected_config_rev": 0,
|
||||
"expected_layout_rev": 0,
|
||||
})
|
||||
response = await client.receive_json()
|
||||
assert response["success"]
|
||||
assert response["result"]["config_rev"] == 1
|
||||
assert response["result"]["layout_rev"] == 1
|
||||
|
||||
intent_write = next(
|
||||
item for item in layout_writes if "optimize_pending" in item
|
||||
)
|
||||
pending = intent_write["optimize_pending"]
|
||||
final_config = await runtime.config_store.async_load()
|
||||
final_layout = await runtime.store.async_load()
|
||||
|
||||
assert pending["config"] == expected["config"]
|
||||
assert pending["layout"] == expected["layout"]
|
||||
assert final_config["config"] == expected["config"]
|
||||
assert final_layout["layout"] == expected["layout"]
|
||||
assert "optimize_pending" not in final_layout
|
||||
assert json.dumps(pending["config"], sort_keys=True, separators=(",", ":")) == \
|
||||
json.dumps(final_config["config"], sort_keys=True, separators=(",", ":"))
|
||||
assert json.dumps(pending["layout"], sort_keys=True, separators=(",", ":")) == \
|
||||
json.dumps(final_layout["layout"], sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
async def test_config_set_validates_new_marker_light_links(
|
||||
hass: HomeAssistant, hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user