From 6ed9aa165214cea5541652428f8c0ffc2b7620cd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 19:30:54 +0300 Subject: [PATCH] test(floor): judge both frames of a held resize drag (#744) smoke_floor_geometry_cache failed in Validate run 36875756451 on one check, ac2cPreviewWallStandsWhereAFreshCardDrawsIt, with every area check green. The check read the moving wall only from the live layer (data-kind "preview"). A host render during a held drag ends that layer: updated() commits it, and nothing repaints it until the next accepted move. The settled scene then draws the preview record itself, and its walls equal those of a fresh card on that record. In the smoke, the "Room updated" toast from the AC1 rename expires 3.5 s after the save, about when the drag starts: locally the rename-to-drag gap is 3.47-3.6 s. On a faster runner the expiry landed after the last move, so the check found no live path. A light toggled in Home Assistant mid-drag gives the same red. The drag now starts once the toast is gone, so the live frame normally judges the live layer. If a host render still lands, the check judges the settled union the same way; a stale union has none of the moved faces. A new check takes the settled frame on purpose: a state change during the held drag, then the union path and the areas must equal the fresh card's. The live strips never pass through the floor-geometry caches, so the old check stayed green with a floor key that ignores the preview. The new check fails on that key, and a failure prints a diagnostic line naming the judged layer and whether the settled union is the stored one. Issue: #744 User-Visible: no Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- demo/smoke_floor_geometry_cache.mjs | 65 +++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/demo/smoke_floor_geometry_cache.mjs b/demo/smoke_floor_geometry_cache.mjs index ef0d002f..0e83945e 100644 --- a/demo/smoke_floor_geometry_cache.mjs +++ b/demo/smoke_floor_geometry_cache.mjs @@ -14,12 +14,17 @@ // floors (the stairs editor used to clear the cache of every floor). // AC2c a live Resize preview on `f1`: the frame follows the preview record, // as the independent card on that record says; cancelling restores the -// walls and areas of the stored record. +// walls and areas of the stored record. Both frames of a held drag are +// judged: the live layer that paints the moving wall, and the settled +// scene that replaces it whenever the host renders mid-drag (an entity +// state change from Home Assistant, a toast expiring) and draws the +// preview record through the floor-geometry caches. // // The independent card is the oracle: a new `houseplan-card` given the same // config through its own `config/get`, with nothing in its caches to reuse. // Card internals are read-only here; writes go through the harness facade, -// the room dialog, the stairs tool and real pointer input. +// the room dialog, the stairs tool, real pointer input and the demo's Home +// Assistant stub (a service call delivers the new entity state). import { launch, checkAll, finish } from './serve.mjs'; import { fixtureWallKey } from './fixtures/wall-key.mjs'; @@ -251,6 +256,17 @@ const res = await page.evaluate(async (fixture) => { // ======== AC2c setup: the Resize tool and the r1/r2 shared wall handle ===== // Outer walls of this fixture are partial (their records span two rooms) and // stay disabled; the shared wall moves the faces of both rooms. + // The drag starts on a quiet card. A host render ends the live layer of a + // held drag (`updated()` commits it) and nothing repaints it until the next + // accepted move. The rename's toast expires 3.5 s after the save; on a fast + // runner that landed after the last move (Validate run 36875756451), and the + // live frame below found no live layer. The settled frame is taken on + // purpose, by a state change, after the live one. + const toastDeadline = performance.now() + 5000; + while (root().querySelector('[data-hp="toast"]') && performance.now() < toastDeadline) + await new Promise((done) => setTimeout(done, 30)); + out.ac2cStartsWithoutAToast = !root().querySelector('[data-hp="toast"]'); + await settled(); await hp.setTool('resize'); diag.ac2cStored = shown(); const handle = [...root().querySelectorAll('.rszhandle:not(.rszcorner)')].find((node) => @@ -283,6 +299,20 @@ if (gesture) { .map((label) => [label.roomId, label.text]); return { ac2cPreviewIsLive: !!preview && preview.space === 'f1' && card._resize.dragging }; })); + // The settled frame of the held drag: a light switched in the house renders + // the host; the settled scene now draws the walls and areas of the preview. + Object.assign(checks, await page.evaluate(async () => { + const card = window.__card; + const { diag, shown, settled } = window.__hp744; + const record = JSON.stringify(card._resize.preview?.sp ?? null); + await card.hass.callService('light', 'toggle', { entity_id: 'light.ceiling' }); + await settled(); + diag.ac2cSettled = shown(); + return { + ac2cStateChangeKeepsThePreview: card._resize.dragging + && JSON.stringify(card._resize.preview?.sp ?? null) === record, + }; + })); await page.keyboard.press('Escape'); await page.mouse.up(); Object.assign(checks, await page.evaluate(async () => { @@ -303,14 +333,30 @@ if (gesture) { out.ac2cPreviewMovesBothRooms = diag.ac2cPreview.areas.r1 !== diag.ac2cStored.areas.r1 && diag.ac2cPreview.areas.r2 !== diag.ac2cStored.areas.r2; // The live layer draws the moving wall; its faces are where the fresh - // card on the preview record draws them, and not where they were. + // card on the preview record draws them, and not where they were. Should + // a host render still land after the last move, the settled scene draws + // the drag instead, and its walls are judged the same way: a stale + // (stored) union has none of the moved faces. const numbers = (text) => new Set((text.match(/-?\d+(?:\.\d+)?/g) || []).map(Number)); + const parts = (walls, kind) => walls.split(' | ').filter((part) => part.startsWith(`${kind}:`)); const stored = numbers(diag.ac2cStored.walls); const moved = [...numbers(fresh.walls)].filter((value) => !stored.has(value)); - const previewPath = numbers(diag.ac2cPreview.walls.split(' | ') - .filter((part) => part.startsWith('preview:')).join(' ')); - out.ac2cPreviewWallStandsWhereAFreshCardDrawsIt = moved.length > 0 - && moved.every((value) => previewPath.has(value)); + const live = parts(diag.ac2cPreview.walls, 'preview'); + const drawn = numbers((live.length ? live : parts(diag.ac2cPreview.walls, 'union')).join(' ')); + const missing = moved.filter((value) => !drawn.has(value)); + out.ac2cPreviewWallStandsWhereAFreshCardDrawsIt = moved.length > 0 && !missing.length; + // The settled scene mid-drag is the fresh card's frame on the preview + // record: the same union path, the same areas. A key that ignores the + // preview hands it the stored union and contours. + const union = (frame) => parts(frame?.walls || '', 'union').join(' | '); + const settledUnion = union(diag.ac2cSettled); + out.ac2cSettledFrameEqualsAFreshCard = !!settledUnion && settledUnion === union(fresh) + && JSON.stringify(diag.ac2cSettled.areas) === JSON.stringify(fresh.areas); + diag.ac2cJudged = { + layer: live.length ? 'preview' : 'union', moved: moved.length, missing, + settledUnion: !settledUnion ? 'none' : settledUnion === union(fresh) ? 'fresh' + : settledUnion === union(diag.ac2cStored) ? 'stored' : 'other', + }; // Room areas through the cached contour, and the live labels beside the // moving wall, are the fresh card's numbers for the preview record. out.ac2cPreviewAreasEqualAFreshCard = JSON.stringify(diag.ac2cPreview.areas) === JSON.stringify(fresh.areas); @@ -320,6 +366,11 @@ if (gesture) { diag.ac2cCancelled = cancelled; return out; })); + if (!checks.ac2cPreviewWallStandsWhereAFreshCardDrawsIt || !checks.ac2cSettledFrameEqualsAFreshCard) { + // Validate prints only `diagnostic` lines and the tail of a failed smoke. + const judged = await page.evaluate(() => window.__hp744.diag.ac2cJudged); + console.log(`diagnostic ac2c: ${JSON.stringify(judged)}`); + } } checkAll(checks, {