// #199: the production bundle must fail closed before the Optimize WS write,
// keep one exact-candidate preflight result, and preserve the existing green
// Preview → Apply / no-op contract.
import { launch, checkAll, finish } from './serve.mjs';
const { page, browser } = await launch({ width: 920, height: 840 });
const out = await page.evaluate(async () => {
const result = {};
const card = window.__card;
const clone = (value) => structuredClone(value);
const noisy = [[
[0.10000000000000002, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9],
]];
const makeSpace = (id, title) => ({
id, title, view_box: [0, 0, 1, 1], cell_cm: 5,
rooms: noisy.map((poly, index) => ({ id: `room-${id || index}`, poly })),
});
const original = {
model_version: 6,
spaces: [
makeSpace('alpha', 'Alpha'),
makeSpace('beta', ''),
makeSpace('', ''),
makeSpace('unsafe', '
'),
],
markers: [], settings: {},
};
const originalLayout = { marker: { s: 'alpha', x: 0.2, y: 0.2 } };
const baseCall = card.hass.callWS.bind(card.hass);
const sent = [];
card.hass = {
...card.hass,
callWS: async (message) => {
if (message.type === 'houseplan/plan/optimize') {
sent.push(clone(message));
return { ok: true, config_rev: 42, layout_rev: 24, can_undo: true };
}
return baseCall(message);
},
};
const setCandidate = (language = 'en') => {
card._config = { ...card._config, language };
card._serverCfg = clone(original);
card._layout = clone(originalLayout);
card._cfgRev = 10;
card._layoutRev = 11;
card._canOptimizeUndo = false;
card._undoKind = null;
card._modelCache = null;
card._frame = null;
card._space = 'alpha';
};
const originalCheck = card._checkOptimizeGeometry.bind(card);
let checks = 0;
let forceRed = true;
card._checkOptimizeGeometry = (config) => {
checks++;
const checked = originalCheck(config);
if (!forceRed) return checked;
const failures = checked.spaces.map((space) => ({
...space, status: 'failed', reason: 'wall-null',
}));
return { ...checked, spaces: failures, failures, ok: false };
};
setCandidate('en');
const beforeRed = {
config: clone(card._serverCfg), layout: clone(card._layout),
configRev: card._cfgRev, layoutRev: card._layoutRev,
canUndo: card._canOptimizeUndo, undoKind: card._undoKind,
};
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const englishText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.englishFailureNamesThreeAndCountsRest = englishText.includes(
'Could not safely verify the geometry of the following spaces: Alpha, beta, Space 3, and 1 more.',
);
result.englishFailureHasExactHint = englishText.includes(
'Plans were not changed. Copy the diagnostics with the button below and attach them to the bug report together with a space export.',
);
// #295: every failure (up to 10) is listed with its reason; the injected
// title renders as inert text — lit escapes it, no element is created.
result.failureListsReasonsAndEscapesTitle =
!card.renderRoot.querySelector('#preflight-injection')
&& englishText.includes('Alpha: The wall body did not build (the union came back empty)')
&& englishText.includes('
');
result.failureRendersNoApply = !card.renderRoot.querySelector('hp-dialog .btn.on');
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.redPreflightMakesZeroWrites = sent.length === 0;
result.redPreflightPreservesAllState =
JSON.stringify(card._serverCfg) === JSON.stringify(beforeRed.config)
&& JSON.stringify(card._layout) === JSON.stringify(beforeRed.layout)
&& card._cfgRev === beforeRed.configRev && card._layoutRev === beforeRed.layoutRev
&& card._canOptimizeUndo === beforeRed.canUndo && card._undoKind === beforeRed.undoKind;
// A green preview whose exact candidate changes must be checked again. A
// newly red result replaces the dialog and still cannot write.
forceRed = false;
setCandidate('en');
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const checksAfterGreenPreview = checks;
result.greenPreviewOffersApply = !!card.renderRoot.querySelector('hp-dialog .btn.on');
card._alignDialog.config.spaces[0].title = 'Changed after preview';
forceRed = true;
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.changedFingerprintRechecks = checks === checksAfterGreenPreview + 1;
result.changedFingerprintFailsClosed = sent.length === 0
&& !card._alignDialog.preflight.ok
&& !card.renderRoot.querySelector('hp-dialog .btn.on');
// An unchanged green candidate reuses the preview result, performs exactly
// one existing atomic endpoint call, and keeps the existing Undo contract.
forceRed = false;
setCandidate('en');
const callsBeforeGreen = sent.length;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const checksBeforeApply = checks;
const previewCandidate = clone(card._alignDialog.config);
await card._editorRuntime.optimizePlans.run(); await card.updateComplete;
result.unchangedApplyDoesNotRecheck = checks === checksBeforeApply;
result.greenApplyMakesOneAtomicWrite = sent.length === callsBeforeGreen + 1
&& JSON.stringify(sent.at(-1).config) === JSON.stringify(previewCandidate);
result.greenApplyPreservesUndoContract = card._canOptimizeUndo === true
&& card._undoKind === 'optimize' && card._cfgRev === 42 && card._layoutRev === 24;
// The just-written candidate is now a no-op: it skips the geometry pass and
// retains the established message/absence of Apply.
const checksBeforeNoOp = checks;
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const noOpText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.noOpSkipsPreflight = checks === checksBeforeNoOp
&& card._alignDialog.preflight === null;
result.noOpKeepsExistingUi = noOpText.includes(
'All plans already use the current optimized data model.',
) && !card.renderRoot.querySelector('hp-dialog .btn.on');
card._alignDialog = null; await card.updateComplete;
// The same bounded copy and fallback/count policy is localized in Russian.
forceRed = true;
setCandidate('ru');
// #627: a live switch to ru holds the previous frame until the lazy editor
// dictionaries settle; the dialog is judged after the language gate.
card.requestUpdate(); await card.updateComplete;
for (let i = 0; i < 250 && card.hasAttribute('aria-busy'); i++) await new Promise((r) => setTimeout(r, 20));
card._editorRuntime.optimizePlans.open(); await card.updateComplete;
const russianText = card.renderRoot.querySelector('hp-dialog .body')?.textContent || '';
result.russianFailureHasExactCopy = russianText.includes(
'Не удалось безопасно проверить геометрию следующих пространств: Alpha, beta, Пространство 3 и ещё 1.',
) && russianText.includes(
'Планы не изменены. Скопируйте диагностику кнопкой ниже и приложите её к отчёту об ошибке вместе с экспортом пространства.',
) && russianText.includes(
'Alpha: Тело стен не построилось (объединение вернуло пустоту)',
);
card.renderRoot.querySelector('hp-dialog')?.dispatchEvent(new CustomEvent(
'hp-close', { bubbles: true, composed: true },
));
await card.updateComplete;
result.failureCloseDoesNotWrite = card._alignDialog === null && sent.length === callsBeforeGreen + 1;
return result;
});
await finish(browser, checkAll(out));