ci: prove the screenshot environment instead of trusting the place

Правило приёмки скриншотов было про место: снимать только в CI. Обоснование
измерено — съёмка в другом окружении переписывает файлы без содержательных
изменений, в #231 два из девяти на 7–8 байт, набор с беты все девять. Но
держалось правило на комментарии, а не на механизме: кандидат проверялся на
самосогласованность и принимался целиком, ни разу не сравниваясь с тем, что
лежит в репозитории.

Цена видна на #390: правка типов, которая физически не может сдвинуть
пиксель, потребовала прогона workflow, а затем правки одиннадцати полей
манифеста руками.

Теперь правило про доказательство, и оно то же, что у golden с #334: среда
доказана, если каждый кадр, который менять не собирались, совпал с
закоммиченным байт-в-байт. Расхождение растеризации спрятать нельзя — оно
задевает все кадры с текстом сразу. Снимать можно где угодно, включая WSL;
принять получится только оттуда, где кадры воспроизводятся, и перестанет
получаться в тот день, когда обновятся шрифты.

Остальное следует из того же правила: намерение объявляется
--expect-change, необъявленное расхождение останавливает приёмку,
объявленное без расхождения — тоже (ложная декларация обесценивает список),
заменяются ровно объявленные файлы, а тотальная перерисовка требует
--no-witnesses --reason, и причина уезжает в манифест.

Частый случай закрылся сам: ничего не объявлено, все кадры совпали —
принимается один манифест, руками ничего писать не надо.

Проверено шестью сквозными прогонами на подделанном артефакте, не только
юнитами: идентичный кандидат, необъявленное расхождение, объявленное,
молчаливая декларация, тотальная перерисовка без причины и с ней.

Issue: #401
User-Visible: no
This commit is contained in:
Matysh
2026-08-31 09:40:52 +03:00
parent 4683a493cc
commit 5c8cb58e1f
5 changed files with 334 additions and 11 deletions
+108
View File
@@ -0,0 +1,108 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
docsAcceptancePlan, docsSilentDeclarations, docsUnknownDeclarations, docsWitnessFloor,
} from '../scripts/docs-acceptance.mjs';
// #401. Правило приёмки скриншотов перестало быть про место съёмки и стало про
// доказательство: среда доказана, если каждый кадр, который менять не
// собирались, совпал с закоммиченным байт-в-байт. Здесь закреплён каждый отказ
// и каждый путь приёмки — иначе правило живёт только в комментарии, как жило
// предыдущее.
const IDS = ['alpha', 'beta', 'gamma'];
const same = { alpha: 'a', beta: 'b', gamma: 'c' };
test('ничего не объявлено и всё совпало — принимается один манифест (#401)', () => {
// Частый случай: правка исходников, которая не может сдвинуть пиксель.
// Раньше он требовал прогона workflow и правки манифеста руками (#390).
const plan = docsAcceptancePlan({ ids: IDS, committed: same, candidate: { ...same } });
assert.equal(plan.refusal, null);
assert.deepEqual(plan.replace, []);
assert.deepEqual(plan.keep, IDS);
assert.equal(plan.witnesses.length, 3);
});
test('объявленный кадр заменяется, остальные не трогаются (#401)', () => {
const plan = docsAcceptancePlan({
ids: IDS, committed: same, candidate: { ...same, beta: 'иное' }, declared: ['beta'],
});
assert.equal(plan.refusal, null);
assert.deepEqual(plan.replace, ['beta']);
// Половина принятого набора хуже непринятого: рядом окажется кадр от одного
// дерева и манифест от другого.
assert.deepEqual(plan.keep, ['alpha', 'gamma']);
assert.deepEqual(plan.witnesses, ['alpha', 'gamma']);
});
test('расхождение без декларации останавливает приёмку (#401)', () => {
const plan = docsAcceptancePlan({ ids: IDS, committed: same, candidate: { ...same, gamma: 'иное' } });
assert.match(plan.refusal, /разошлись, но не объявлены: gamma/);
// Отказ обязан называть оба объяснения: автор не знает, какое из них его.
assert.match(plan.refusal, /изменение продукта/);
assert.match(plan.refusal, /другой среде/);
assert.deepEqual(plan.replace, []);
});
test('молчаливая декларация — тоже отказ (#401)', () => {
const plan = docsAcceptancePlan({
ids: IDS, committed: same, candidate: { ...same, beta: 'иное' }, declared: ['beta', 'alpha'],
});
assert.match(plan.refusal, /не изменились: alpha/);
});
test('объявленного имени нет в наборе — отказ до всякой проверки (#401)', () => {
const plan = docsAcceptancePlan({
ids: IDS, committed: same, candidate: { ...same }, declared: ['опечатка'],
});
assert.match(plan.refusal, /которых нет в наборе: опечатка/);
});
test('тотальная перерисовка требует явного обхода с причиной (#401)', () => {
const all = { alpha: 'x', beta: 'y', gamma: 'z' };
const declared = [...IDS];
const refused = docsAcceptancePlan({ ids: IDS, committed: same, candidate: all, declared });
assert.match(refused.refusal, /свидетелей недостаточно: 0 из необходимых 1/);
const noReason = docsAcceptancePlan({
ids: IDS, committed: same, candidate: all, declared, skipWitnesses: true,
});
assert.match(noReason.refusal, /требует --reason/);
const bypassed = docsAcceptancePlan({
ids: IDS, committed: same, candidate: all, declared,
skipWitnesses: true, skipReason: 'сменился шрифтовый стек',
});
assert.equal(bypassed.refusal, null);
assert.deepEqual(bypassed.replace, IDS);
});
test('порог свидетелей считается так же, как в golden (#401)', () => {
// Два набора картинок в одном репозитории не должны требовать от человека
// помнить два разных правила.
assert.equal(docsWitnessFloor(0), 0);
assert.equal(docsWitnessFloor(10), 1);
assert.equal(docsWitnessFloor(143), 10);
});
test('вспомогательные проверки называют виновников по именам (#401)', () => {
assert.deepEqual(docsUnknownDeclarations(IDS, ['gamma', 'нет-такого']), ['нет-такого']);
assert.deepEqual(
docsSilentDeclarations({ committed: same, candidate: { ...same }, declared: ['beta', 'alpha'] }),
['alpha', 'beta'],
);
});
test('кадр без закоммиченной пары не может быть свидетелем (#401)', () => {
// Первичная съёмка ничего не доказывает про среду: сравнивать не с чем.
const plan = docsAcceptancePlan({
ids: IDS,
committed: { alpha: 'a' },
candidate: { alpha: 'a', beta: 'новое', gamma: 'новое' },
declared: ['beta', 'gamma'],
});
assert.equal(plan.refusal, null);
assert.deepEqual(plan.witnesses, ['alpha']);
assert.equal(plan.floor, 1);
});