mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 23:19:14 +00:00
test(gates): мутанты на восемь модулей горячего пути из #451
#451 принёс 1 651 строку в восьми новых модулях, а единственный мутант того релиза патчил houseplan-card.ts и houseplan-editor-runtime.ts — места, ОТКУДА код ушёл. Вынос в модули был правильным решением, но защита осталась смотреть на старые файлы. Восемь мутантов, семь модулей, все прогнаны штатным раннером (чистый прогон зелёный, мутант красный): - live-editor-view-mode-routes-live — режим View обязан оставаться реактивным: живой путь редактора там означает план, который перестал отвечать на Home Assistant у двух персон из трёх; - live-editor-first-gesture-frame-goes-live — первый кадр жеста меняет выделение и хром и обязан остаться реактивным; - pointer-move-queue-keeps-first-move — очередь last-wins: сохранение ПЕРВОГО коллбэка кадра рисует план там, где палец уже не находится; - live-hass-tick-never-deferred — тик состояния посреди жеста откладывается намеренно, и флаг отложенности гарантирует его воспроизведение после; - live-viewport-identity-projection-not-recognized — см. ниже; - render-invalidation-unknown-key-ignored — классификатор обязан ошибаться в сторону перерисовки на незнакомых ключах hass; - resize-live-preflight-keeps-every-room — живой resize проверяет только задетые комнаты, иначе каждый кадр становится полной проверкой плана; - render-lifecycle-diagnostics-cache-never-invalidated — кэш диагностики обязан сбрасываться, иначе красная точка нового устройства не загорится. Мутант на live-viewport пришлось заменить, и это стоит записать. Issue предлагал снять `setLayerProjection(layer, null)` из `commitHouseplanViewport` — прогон показал, что тест остаётся ЗЕЛЁНЫМ: следом идёт `paintLiveViewport(root, painted, painted)`, который на равных аргументах даёт identity и обнуляет проекцию сам. Тот цикл — подстраховка, а не контракт. Настоящий контракт — распознавание identity (`isIdentityLiveLayerProjection`), потому что даже единичный transform переключает путь композитинга и сдвигает установившийся растр на несколько уровней цвета. Мутируется теперь он. `src/live-hover.ts` остался без мутанта сознательно: его контракты либо чисто производительные (мемо по наведённой комнате), либо доменные (подсветка комнаты, застрявшая после ухода курсора). Первое мутантом не ловится в принципе, второе — только браузерным смоком, которого в песочнице нет. Записано в тесте и в issue. Чтобы требование не жило в памяти, добавлен гейт: у каждого модуля горячего пути обязан быть свой мутант, и он не имеет права патчить houseplan-card.ts или houseplan-editor-runtime.ts — то есть исходный дефект #458 больше не воспроизводим молча. Проверено отрицательным прогоном: перевод патча любого из модулей на старое ядро краснит гейт. Гейты: npm test 1960 tests, 1959 pass, 0 fail; typecheck зелёный; mutation-gate --check зелёный на всех якорях; каждый из восьми мутантов прогнан отдельно. Issue: #458 User-Visible: no
This commit is contained in:
@@ -5766,6 +5766,116 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " if (doubleFit) this._resetZoom('double-tap');",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'live-editor-view-mode-routes-live',
|
||||
guard: 'node --test test/live-editor.test.mjs',
|
||||
because: 'View is the product for two of three personas: the live editor path must never '
|
||||
+ 'take over rendering there, or a plan nobody is editing stops reacting to Home '
|
||||
+ 'Assistant (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/live-editor.ts',
|
||||
find: " if (!host?.isConnected || host._mode === 'view') return false;",
|
||||
replace: ' if (!host?.isConnected) return false;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'live-editor-first-gesture-frame-goes-live',
|
||||
guard: 'node --test test/live-editor.test.mjs',
|
||||
because: 'the first frame of a gesture may change selection and chrome, so it must stay '
|
||||
+ 'reactive; routing it live leaves the newly selected object drawn in its previous '
|
||||
+ 'state until the gesture ends (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/live-editor.ts',
|
||||
find: ' if (name !== undefined && gestureProperties.has(name)\n'
|
||||
+ ' && oldValue == null\n'
|
||||
+ ' && (host as unknown as Record<PropertyKey, unknown>)[name] != null\n'
|
||||
+ ' && activeEditorGesture(host)) {\n'
|
||||
+ ' // Pointerdown may change selection/chrome once. Subsequent moves stay live.\n'
|
||||
+ ' return false;\n'
|
||||
+ ' }\n',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'pointer-move-queue-keeps-first-move',
|
||||
guard: 'node --test test/live-editor.test.mjs',
|
||||
because: 'the coalescing queue is last-wins: keeping the FIRST callback of an event turn '
|
||||
+ 'means the pointer moves on and the plan paints where the finger used to be (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/pointer-move-queue.ts',
|
||||
find: ' if (queued) {\n queued.run = run;\n return;\n }',
|
||||
replace: ' if (queued) {\n return;\n }',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'live-hass-tick-never-deferred',
|
||||
guard: 'node --test test/houseplan-render-lifecycle.test.mjs',
|
||||
because: 'a state tick arriving mid-gesture is deferred on purpose; rendering it '
|
||||
+ 'immediately drops frames of the gesture the user is performing, and the deferral flag '
|
||||
+ 'is what guarantees the skipped tick is replayed afterwards (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/live-interaction-runtime.ts',
|
||||
find: " const defer = change === 'state' && this.active();",
|
||||
replace: ' const defer = false;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'live-viewport-identity-projection-not-recognized',
|
||||
guard: 'node --test test/live-viewport.test.mjs',
|
||||
because: 'a settled viewport must be recognized as identity and leave NO compositor '
|
||||
+ 'transform: an identity transform still switches the browser compositing path and '
|
||||
+ 'shifts the settled raster by a few colour levels, which is exactly what makes golden '
|
||||
+ 'frames flap (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/live-viewport.ts',
|
||||
// Первая редакция этого мутанта снимала `setLayerProjection(layer, null)`
|
||||
// из `commitHouseplanViewport` (так предлагал issue) — и тест оставался
|
||||
// ЗЕЛЁНЫМ: следом идёт `paintLiveViewport(root, painted, painted)`, а он
|
||||
// на равных аргументах даёт identity и обнуляет проекцию сам. То есть тот
|
||||
// цикл не контракт, а подстраховка. Настоящий контракт — распознавание
|
||||
// identity, и мутируется именно оно.
|
||||
find: ' projection.translateXPercent === 0',
|
||||
replace: ' projection.translateXPercent === 1',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'render-invalidation-unknown-key-ignored',
|
||||
guard: 'node --test test/render-invalidation.test.mjs',
|
||||
because: 'the classifier fails OPEN on Home Assistant keys it does not know: without that '
|
||||
+ 'rule a new HA capability silently leaves the plan stale, and a stale plan looks '
|
||||
+ 'exactly like a working one (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/render-invalidation.ts',
|
||||
find: ' for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {\n'
|
||||
+ " if (!compared.has(key) && before[key] !== after[key]) return 'structural';\n"
|
||||
+ ' }\n',
|
||||
replace: '',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'resize-live-preflight-keeps-every-room',
|
||||
guard: 'node --test test/resize-controller.test.mjs',
|
||||
because: 'live resize checks only the rooms the gesture touches; widening that to the whole '
|
||||
+ 'plan turns every frame into a full-plan geometry check and the drag stutters on large '
|
||||
+ 'plans — the very regression #451 removed (#458)',
|
||||
patches: [{
|
||||
file: 'src/resize-live-preflight.ts',
|
||||
find: ' return id && changed.has(id) ? [id] : [];',
|
||||
replace: ' return id ? [id] : [];',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'render-lifecycle-diagnostics-cache-never-invalidated',
|
||||
guard: 'node --test test/houseplan-render-lifecycle.test.mjs',
|
||||
because: 'the diagnostics cache must drop when the registry or a tracked state appears or '
|
||||
+ 'disappears; a cache that never invalidates reports yesterday binding health as todays '
|
||||
+ 'and the red dot for a silently added device never lights (#451, #458)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-render-lifecycle.ts',
|
||||
find: ' if (!this.diagnosticsCache) return;\n',
|
||||
replace: ' if (!this.diagnosticsCache) return;\n if (true) return;\n',
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user