From 601e68e52d6d6463d1c90ddf8a49a1584f3f3c7f Mon Sep 17 00:00:00 2001 From: Matysh Date: Tue, 1 Sep 2026 17:19:41 +0300 Subject: [PATCH] fix: six smokes never asked whether the card threw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Счётчик исключений внутри карточки живёт в demo/serve.mjs, и читала его одна функция — finish(). Шесть смоков её не вызывали вовсе: у трёх своя развязка (`if (!ok) process.exit(1)`), у двух throw из try/finally, у smoke_entry_stale ни того ни другого. Необработанное исключение во время этих шести проходило незамеченным всегда — в лог печаталось EXC, а прогон оставался зелёным. smoke_entry_stale был хуже остальных: он складывал неудачи в _failures через check/checkAll, но их никто не печатал и код возврата не выставлял. То есть смок не мог провалиться в принципе — ровно паттерн «печатали булевы значения и всегда выходили нулём», который шапка serve.mjs описывает как исправленный в 2026-07-27. Добавлен reportPageErrors(): тот же вердикт, что у finish(), для смоков со своей логикой выхода. Каждый из шести теперь вердикт запрашивает, а smoke_entry_stale получил finish() и вместе с ним настоящий код возврата. Вердикт обязан ОСТАНАВЛИВАТЬ, а не только помечать. Первый заход выставлял process.exitCode, и отрицательный прогон напечатал «FAILED: 1 uncaught exception(s)» и следом «OK deep-link: …»: код был верным, вывод противоречивым, а читают вывод. Доказано отрицательным прогоном, а не рассуждением: на ветке experiment/407-negative smoke_deeplink получил намеренное исключение внутри карточки, шард 2/3 упал с exit code 1, в логе FAIL и FAILED без строки успеха. Ветка удалена. Гейт против повторения — test/smoke-harness-contract.test.mjs: он падает, если смок не запрашивает вердикт или запрашивает, не останавливаясь. На origin/dev до починки он находил ровно шесть файлов, после — ноль. Issue: #407 User-Visible: no --- demo/serve.mjs | 26 ++++++++++ demo/smoke_deeplink.mjs | 5 +- demo/smoke_entry_stale.mjs | 10 ++-- demo/smoke_glow_blending.mjs | 6 ++- demo/smoke_icon_center.mjs | 5 +- demo/smoke_long_press_gesture.mjs | 6 ++- demo/smoke_space_card.mjs | 5 +- test/smoke-harness-contract.test.mjs | 72 ++++++++++++++++++++++++++++ 8 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 test/smoke-harness-contract.test.mjs diff --git a/demo/serve.mjs b/demo/serve.mjs index 6f92bca8..828629db 100644 --- a/demo/serve.mjs +++ b/demo/serve.mjs @@ -31,6 +31,32 @@ export function checkAll(out, expected = {}) { return out; } +/** + * Тот же вердикт по исключениям в карточке, что у `finish()`, для смоков со + * своей логикой выхода (#407). + * + * Счётчик `_pageErrors` живёт здесь, а читал его только `finish()`. Шесть + * смоков `finish()` не вызывали вовсе — у каждого была своя ручная развязка + * (`if (!ok) process.exit(1)`) либо `throw` из try/finally, и ни одна не + * спрашивала про исключения. То есть необработанное исключение внутри карточки + * во время этих шести проходило незамеченным всегда: в лог печаталось `EXC`, а + * прогон оставался зелёным. + * + * Функция ничего не бросает намеренно. Смок с ручной развязкой продолжает + * решать сам, что печатать и когда выходить; здесь только выставляется код + * возврата, который дальше уже не отнять — `process.exitCode` переживёт любой + * последующий `console.log`. + * + * @returns true, если карточка бросала — чтобы вызывающий мог добавить своё + * сообщение, не считая исключения заново. + */ +export function reportPageErrors() { + if (!_pageErrors) return false; + console.error(`FAILED: ${_pageErrors} uncaught exception(s) inside the card`); + process.exitCode = 1; + return true; +} + /** Print the result, report failures, close the browser, set the exit code. */ export async function finish(browser, out) { if (out !== undefined) console.log(JSON.stringify(out, null, 1)); diff --git a/demo/smoke_deeplink.mjs b/demo/smoke_deeplink.mjs index 1ac33390..9f6fcaa9 100644 --- a/demo/smoke_deeplink.mjs +++ b/demo/smoke_deeplink.mjs @@ -1,5 +1,5 @@ // Smoke: the full houseplan-card honours the #space= deep-link (hashchange). -import { launch } from './serve.mjs'; +import { launch, reportPageErrors } from './serve.mjs'; const { page, browser } = await launch({ width: 820, height: 760 }, 1); const res = await page.evaluate(async () => { const card = window.__card; @@ -22,5 +22,8 @@ const res = await page.evaluate(async () => { await browser.close(); const ok = res.after === res.target && res.afterBad === res.target; console.log(JSON.stringify(res)); +// #407: своя развязка про исключения в карточке не спрашивает. Вердикт обязан +// именно остановить: иначе строка успеха печатается после «FAILED». +if (reportPageErrors()) process.exit(1); if (!ok) { console.error('FAIL deeplink smoke'); process.exit(1); } console.log('OK deep-link: full card switches to #space=, ignores invalid ids'); diff --git a/demo/smoke_entry_stale.mjs b/demo/smoke_entry_stale.mjs index 615bbd06..fcb9daf2 100644 --- a/demo/smoke_entry_stale.mjs +++ b/demo/smoke_entry_stale.mjs @@ -5,7 +5,7 @@ // and `await import(entry)` + `createElement(...).setConfig(...)` stays valid. import { readFileSync } from 'node:fs'; import { chromium } from 'playwright'; -import { check, checkAll } from './serve.mjs'; +import { check, checkAll, finish } from './serve.mjs'; const entry = readFileSync('dist/houseplan-card.js'); const page404 = `