Files
houseplan-card/scripts/golden-acceptance.mjs
T
Claude 4800c44e51 test: локальная съёмка golden допустима по доказательству, а не по доверию
Прежде эталон принимался только из артефакта CI: растеризация шрифтов на другой
машине может отличаться, а доказать обратное было нечем. Цена — два полных
прогона на каждый визуальный фикс, при версии матрицы 48 она платится часто.

Доказательство теперь эмпирическое: среда равна раннеру, если каждая сцена,
которую менять не собирались, совпала со своим эталоном. Расхождение
растеризации спрятать нельзя — оно задевает все сцены с текстом. Ревьюер
объявляет намерение через --expect-change, всё разошедшееся помимо списка
приёмку запрещает. Поэтому неверно угаданный тег образа не может испортить
эталоны: он может только не сработать.

То же правило независимо от среды запрещает «принять всё, чтобы CI позеленел» —
именно так эталон перестаёт быть эталоном, молча и одной командой.

scripts/golden-container.mjs снимает кандидатов в образе Playwright той же
версии, что залочена в package-lock. Хозяйский node_modules прячется анонимным
томом: он собран под Windows, и npm ci внутри контейнера сломал бы дерево.

Обёртка, а не правка demo/golden/accept.mjs, — намеренно. sourceFingerprint
включает ВСЕ .mjs из demo/golden, включая accept.mjs и policy.mjs, которые
исполняются после съёмки и ни одного пикселя изменить не могут. Их правка
объявляет устаревшими бандл и оба манифеста, то есть требует ровно того двойного
цикла, который эта задача убирает. Сужение корпуса отпечатка — отдельная задача:
сам source-fingerprint.mjs в корпусе, и одна пересборка бандла неизбежна.

Issue: #334
User-Visible: no
2026-08-28 01:59:45 +03:00

60 lines
4.5 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.
/**
* Правило допустимости съёмки golden-кандидатов (#334).
*
* Прежде эталон принимался только из артефакта CI: растеризация шрифтов на
* другой машине может отличаться, а доказать обратное было нечем. Цена —
* двойной roundtrip на каждый визуальный фикс: пуш, семь минут CI, скачивание
* артефакта, приёмка, второй пуш, второй полный прогон. При версии матрицы 48
* это платится регулярно.
*
* Доказательство есть, и оно эмпирическое: среда съёмки равна раннеру, если
* КАЖДАЯ сцена, которую менять не собирались, совпала со своим принятым
* эталоном. Расхождение растеризации нельзя спрятать — оно задевает все сцены
* с текстом, а не только правленные. Поэтому ревьюер объявляет намерение
* списком `--expect-change`, и всё, что разошлось помимо списка, приёмку
* запрещает: либо это регрессия рендера, либо среда не та.
*
* То же правило ловит другое, независимо от среды: «принять всё, чтобы CI
* позеленел». Именно так эталон перестаёт быть эталоном — молча, одной
* командой, без единого названного намерения.
*
* `missing-baseline` объявления не требует: у новой сцены нет эталона, которому
* она могла бы противоречить.
*
* Почему это лежит в `scripts/`, а не рядом с `accept.mjs`. Отпечаток
* `sourceFingerprint` включает ВСЕ `.mjs` из `demo/golden`, включая
* `accept.mjs` и `policy.mjs`, которые исполняются после того, как картинка уже
* снята, и ни одного пикселя изменить не могут. Правка любого из них объявляет
* устаревшими и закоммиченный бандл, и манифест скриншотов документации, и
* манифест эталонов — то есть требует ровно того двойного цикла, который эта
* задача убирает. Сужение корпуса отпечатка — отдельная задача: оно неизбежно
* требует пересборки бандла, потому что сам `source-fingerprint.mjs` в корпусе.
*/
export const goldenAcceptanceRefusal = (results, declared = []) => {
if (!Array.isArray(results)) return 'отчёт кандидатов не содержит результатов сцен';
const expected = new Set(declared.filter(Boolean));
const known = new Set(results.map((result) => result.id));
const unknown = [...expected].filter((id) => !known.has(id));
if (unknown.length) {
return `в --expect-change названы сцены, которых нет в отчёте: ${unknown.sort().join(', ')}`;
}
const undeclared = results
.filter((result) => result.status === 'different' && !expected.has(result.id))
.map((result) => result.id)
.sort();
if (undeclared.length) {
return 'съёмка разошлась с принятыми эталонами в сценах, которые менять не собирались:'
+ ` ${undeclared.join(', ')}.`
+ ' Либо это регрессия рендера, либо среда съёмки не совпадает с раннером —'
+ ' в обоих случаях приёмка запрещена. Намеренные сцены перечисляются в'
+ ' --expect-change=<id,id>';
}
return null;
};
/** Сцены, объявленные изменёнными, но совпавшие с эталоном: не ошибка, но и не молчание. */
export const goldenSilentDeclarations = (results, declared = []) => declared
.filter((id) => results.find((result) => result.id === id)?.status === 'passed')
.sort();