mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 06:08:59 +00:00
Прежде эталон принимался только из артефакта 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
91 lines
4.8 KiB
JavaScript
91 lines
4.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
assertGoldenInvocation,
|
|
GOLDEN_BASELINE_MANIFEST,
|
|
goldenRunFailed,
|
|
goldenScenarioSetsMatch,
|
|
} from '../demo/golden/policy.mjs';
|
|
import {
|
|
goldenAcceptanceRefusal, goldenSilentDeclarations,
|
|
} from '../scripts/golden-acceptance.mjs';
|
|
|
|
test('golden metadata cannot be mistaken for a Home Assistant integration manifest', () => {
|
|
assert.equal(GOLDEN_BASELINE_MANIFEST, 'baselines-index.json');
|
|
// The old check compared against 'manifest.json' and passed while the file
|
|
// was called 'baseline-manifest.json' — which is exactly what the HACS glob
|
|
// `*manifest.json` matches, and what turned Hassfest red on PR #9004
|
|
// (2026-08-11). Ending with the word is enough to break the submission.
|
|
assert.equal(GOLDEN_BASELINE_MANIFEST.endsWith('manifest.json'), false);
|
|
});
|
|
|
|
test('golden capture fails on runtime errors but permits missing baselines', () => {
|
|
assert.equal(goldenRunFailed('capture', false, [{ status: 'error' }]), true);
|
|
assert.equal(goldenRunFailed('capture', false, [{ status: 'missing-baseline' }]), false);
|
|
assert.equal(goldenRunFailed('verify', true, [{ status: 'different' }]), true);
|
|
assert.equal(goldenRunFailed('verify', true, [{ status: 'passed' }]), false);
|
|
});
|
|
|
|
test('golden verification cannot make a partial success claim', () => {
|
|
assert.doesNotThrow(() => assertGoldenInvocation('capture', 'one-scenario'));
|
|
assert.doesNotThrow(() => assertGoldenInvocation('verify', ''));
|
|
assert.throws(() => assertGoldenInvocation('verify', 'one-scenario'), /complete matrix/);
|
|
assert.throws(() => assertGoldenInvocation('unknown', ''), /unknown golden mode/);
|
|
});
|
|
|
|
test('golden baseline inventory rejects orphan hashes and PNGs', () => {
|
|
assert.equal(goldenScenarioSetsMatch(['a', 'b'], ['b', 'a'], ['a', 'b']), true);
|
|
assert.equal(goldenScenarioSetsMatch(['a'], ['a', 'orphan'], ['a']), false);
|
|
assert.equal(goldenScenarioSetsMatch(['a'], ['a'], ['a', 'orphan']), false);
|
|
assert.equal(goldenScenarioSetsMatch(['a', 'b'], ['a'], ['a', 'b']), false);
|
|
});
|
|
|
|
// #334. Локальная съёмка допустима не по доверию, а по доказательству: среда
|
|
// равна раннеру, если всё, что менять не собирались, совпало с эталонами.
|
|
const results = (entries) => entries.map(([id, status]) => ({ id, status }));
|
|
|
|
test('приёмка отвергает расхождение в сцене, которую менять не собирались (#334)', () => {
|
|
const refusal = goldenAcceptanceRefusal(
|
|
results([['a', 'different'], ['b', 'different'], ['c', 'passed']]), ['a'],
|
|
);
|
|
assert.match(refusal, /^съёмка разошлась/);
|
|
assert.match(refusal, / b\./);
|
|
// Объявленная сцена в перечислении не появляется — иначе сообщение
|
|
// указывало бы на автора вместо среды.
|
|
assert.equal(/ a[,.]/.test(refusal), false);
|
|
});
|
|
|
|
test('приёмка проходит, когда разошлись ровно объявленные сцены (#334)', () => {
|
|
assert.equal(goldenAcceptanceRefusal(
|
|
results([['a', 'different'], ['b', 'passed'], ['c', 'missing-baseline']]), ['a'],
|
|
), null);
|
|
});
|
|
|
|
test('новая сцена объявления не требует: эталона, которому противоречить, нет (#334)', () => {
|
|
assert.equal(goldenAcceptanceRefusal(results([['new', 'missing-baseline']]), []), null);
|
|
});
|
|
|
|
test('приёмка без объявлений запрещает «принять всё, чтобы CI позеленел» (#334)', () => {
|
|
// Ровно то, чем эталон перестаёт быть эталоном: одна команда, три подмены,
|
|
// ни одного названного намерения.
|
|
const refusal = goldenAcceptanceRefusal(
|
|
results([['a', 'different'], ['b', 'different'], ['c', 'different']]), [],
|
|
);
|
|
assert.match(refusal, /a, b, c/);
|
|
});
|
|
|
|
test('приёмка отвергает объявление сцены, которой нет в отчёте (#334)', () => {
|
|
const refusal = goldenAcceptanceRefusal(results([['a', 'passed']]), ['a', 'опечатка']);
|
|
assert.match(refusal, /которых нет в отчёте: опечатка/);
|
|
});
|
|
|
|
test('приёмка отвергает отчёт без результатов сцен (#334)', () => {
|
|
assert.match(goldenAcceptanceRefusal(null, []), /не содержит результатов/);
|
|
});
|
|
|
|
test('объявленная, но совпавшая сцена называется, а не проглатывается (#334)', () => {
|
|
const list = results([['a', 'passed'], ['b', 'different']]);
|
|
assert.deepEqual(goldenSilentDeclarations(list, ['a', 'b']), ['a']);
|
|
assert.deepEqual(goldenSilentDeclarations(list, ['b']), []);
|
|
});
|