Files
houseplan-card/demo/guard/verify-guard.mjs
T
Matysh 2903374b72 fix: the exception guard reads its counter after delivery, not before
Гард «uncaught exception внутри карточки» жил в demo/serve.mjs с 2026-07-27 и
не срабатывал ни разу в самом частом случае. Счётчик читался синхронно, а
Playwright доставляет pageerror асинхронно по CDP: если исключение возникло
после последнего обращения смока к странице, счётчик к моменту проверки
нулевой, а browser.close() уносит недоставленное событие. В логе это видно
дословно — EXC печатается после результата и до OK.

finish() теперь делает round-trip по открытым страницам перед чтением
счётчика. Страницы регистрируются там, где создаются: ссылок на них у
finish(browser, out) нет, а менять сигнатуру нельзя — так её зовут 205
смоков.

Medium-1 жёлтого ревью ТЗ закрыт расширением, а не оговоркой. Страницы,
созданные смоком после launch(), регистрация в launchInternal не покрывает:
smoke_zoom_flash печатал своё EXC2 мимо счётчика, три страницы
smoke_svg_sandbox не имели слушателя вовсе. Документировать слепую зону в
задаче, которая существует ради устранения слепой зоны, значит закрыть issue,
оставив дефект. Наружу отдана одна функция watchPage(page): подписка и
регистрация неразделимы, иначе появится страница, чьи исключения считаются, а
доставки не ждёт никто.

Разрыв оказался шире, чем в ревью: проверка по всему набору нашла ещё два
файла со своей подпиской — smoke_cold_view_toggle и smoke_cold_view_vacuum.
Они не слепая зона, их страница приходит из launchColdView и уже
зарегистрирована, а свой счётчик они превращают в отдельное утверждение.
Поэтому инвариант сформулирован как «ни одна страница не создаётся мимо
гарда» и закреплён по всему набору, а не по двум названным файлам.

reportPageErrors() из #407 стал асинхронным: второй читатель счётчика обязан
ждать доставку так же, как finish(). Пять смоков получили await.

Фикстура smoke_danger_confirmation приведена к объявленному типу: без binding
и bindingMode _bindingHasHaPage падал на undefined.split(':') — два
исключения, которых гард не видел. Дефекта поведения нет, все 15 мест в src/,
создающих диалог, binding пишут; врала фикстура.

Два отступления от ТЗ, каждое по измеренной причине. Пробы лежат в
demo/guard/, а не demo/fixtures/: последний входит в корпус sourceFingerprint,
и каждый файл там объявил бы устаревшими бандл, скриншот-индекс и
golden-индекс — пробы же не касаются ни одного пикселя. Поведение
доказывается в job со браузером, а не в npm test: job «Фронтенд» браузеры не
ставит, и тест молча скипался бы — тот самый тихий успех, против которого вся
задача.

Issue: #404
User-Visible: no
2026-09-01 19:08:07 +03:00

67 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Отрицательный прогон гарда исключений (#404).
*
* Гард объявлен в `demo/serve.mjs` с 2026-07-27 и до этой задачи не срабатывал
* ни разу: счётчик читался раньше, чем Playwright доставлял `pageerror`. Такое
* ловится только запуском — «проверка, которая не умеет падать» выглядит
* идентично работающей во всём, кроме исхода.
*
* Почему не тест в `test/`: пробам нужен настоящий Chromium, а job «Фронтенд»,
* где идёт `npm test`, браузеры не ставит. Тест там молча скипался бы — то есть
* ровно тот тихий успех, против которого всё это и делается. Поэтому проверка
* живёт в job «Смоки в браузере», где браузер есть, и вызывается один раз.
*/
import { spawnSync } from 'node:child_process';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
/** Каждая проба: чего ждём от кода возврата и что обязано быть в выводе. */
const PROBES = [
{
file: 'guard_tail_exception.mjs',
expectExit: 1,
expectOutput: /uncaught exception\(s\) inside the card/,
because: 'исключение в хвосте — та самая слепая зона, из-за которой заводился #404',
},
{
file: 'guard_tail_rejection.mjs',
expectExit: 1,
expectOutput: /uncaught exception\(s\) inside the card/,
because: 'отклонение промиса приходит тем же каналом; на этом держится связь с #405',
},
{
file: 'guard_closed_page.mjs',
expectExit: 0,
expectOutput: /OK/,
because: 'round-trip к закрытой странице не имеет права ронять вердикт',
},
];
let failed = 0;
for (const probe of PROBES) {
const run = spawnSync(process.execPath, [resolve(HERE, probe.file)], {
encoding: 'utf8', cwd: resolve(HERE, '../..'), timeout: 90_000,
});
const output = `${run.stdout || ''}${run.stderr || ''}`;
const exitOk = run.status === probe.expectExit;
const textOk = probe.expectOutput.test(output);
if (exitOk && textOk) {
console.log(`ok ${probe.file} → exit ${run.status}`);
continue;
}
failed += 1;
console.error(`FAIL ${probe.file}: ${probe.because}`);
console.error(` ожидался exit ${probe.expectExit}, получен ${run.status}`);
if (!textOk) console.error(` в выводе нет ${probe.expectOutput}`);
console.error(output.split('\n').slice(-12).map((line) => ` | ${line}`).join('\n'));
}
if (failed) {
console.error(`\nгард исключений не доказан: проб провалено ${failed} из ${PROBES.length}`);
process.exit(1);
}
console.log(`\nгард исключений доказан на ${PROBES.length} пробах`);