fix v1.43.0: external audit P0 — data loss, split geometry, auth, dialog zombies

L2 (silent data loss): debounce gains flush()/pending(); _reloadConfigOnly
flushes a pending write and defers while one is in flight; conflict path
forces; failed reload now toasts instead of an empty catch; teardown flushes.

G1 (split corruption): same-edge cuts carve the niche properly instead of
walking the outline twice; partition invariant (parts sum to the original)
rejects anything else; +1 unit test covering 5 niche shapes and both legacy
cut shapes.

B1 (unauthenticated content): plans and marker files move to
HouseplanContentView (/api/houseplan/content/..., requires_auth); only the
card bundle stays static; contentUrl() rewrites legacy URLs on read (no
storage migration); repairs.py accepts both prefixes; +1 unit test.

L3 (dialog zombies): all four save catch-blocks guard against a closed
dialog; the card no longer blanks when a save fails after Esc.

smokes: smoke_save_race, smoke_dialog_zombie; docs (TESTING/CHANGELOG/
ARCHITECTURE incl. the optimistic-UI note) same-commit
This commit is contained in:
Matysh
2026-07-27 10:44:58 +03:00
parent 5c7d1ca8bb
commit 0fd0ba408d
21 changed files with 464 additions and 98 deletions
+36
View File
@@ -0,0 +1,36 @@
import { launch } from './serve.mjs';
const { page, browser } = await launch();
const res = await page.evaluate(async () => {
const out = {};
const c = window.__card;
const sent = [];
// перехват WS: config/set логируем, config/get отдаёт "серверную" копию БЕЗ локальной правки
const server = { cfg: JSON.parse(JSON.stringify(c._serverCfg)), rev: c._cfgRev };
c.hass = { ...c.hass, callWS: async (msg) => {
if (msg.type === 'houseplan/config/set') {
sent.push(JSON.parse(JSON.stringify(msg.config)));
server.cfg = JSON.parse(JSON.stringify(msg.config));
server.rev = (msg.expected_rev ?? server.rev) + 1;
return { rev: server.rev };
}
if (msg.type === 'houseplan/config/get') return { config: JSON.parse(JSON.stringify(server.cfg)), rev: server.rev };
return {};
} };
await c.updateComplete;
// локальная правка (как разметка комнаты) + дебаунс
const sp = c._curSpaceCfg;
sp.rooms.push({ id: 'race_room', name: 'RACE', area: null, poly: [[0.8, 0.8], [0.9, 0.8], [0.9, 0.9], [0.8, 0.9]] });
c._saveConfig();
out.pending = c._saveConfig.pending();
// через 100 мс приходит событие о чужой ревизии — раньше это стирало правку
c._cfgRev = server.rev; // симулируем: наша ревизия отстала
await c._reloadConfigOnly();
await new Promise((r) => setTimeout(r, 900));
// правка обязана уцелеть и уйти на сервер
out.editSent = sent.some((cf) => cf.spaces.some((s) => s.rooms?.some((r) => r.id === 'race_room')));
out.editInMemory = c._serverCfg.spaces.some((s) => s.rooms?.some((r) => r.id === 'race_room'));
out.serverHasIt = server.cfg.spaces.some((s) => s.rooms?.some((r) => r.id === 'race_room'));
return out;
});
console.log(JSON.stringify(res, null, 1));
await browser.close();