mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 22:49:16 +00:00
fix(gates): съёмка кадров в чужой среде отказывается заранее
Вопрос владельца: зачем агенты снимают PNG на Windows, если кадры мы всё равно не принимаем, тем более что WSL есть на обеих машинах. Ответ по коду: этому ничто не мешало. Ни один из шести скриптов съёмки и приёмки не знал, на какой он ОС — ни `process.platform`, ни win32, ни WSL не упоминались нигде. Съёмка отрабатывала штатно, а стена появлялась на приёмке, и текст стены говорил «сцен-свидетелей 0 из 10», то есть подсказывал неверный вывод «надо объявить больше сцен» — от которого до `--expect-change` на всю матрицу одна команда. Причина запрета не политика, а физика: Windows растеризует текст через DirectWrite, с другим субпиксельным сглаживанием и DPI, поэтому байтового совпадения с принятым эталоном не даёт никогда, свидетелей среды быть не может, и приёмка откажет всё равно. Флаги детерминизма из #410 убирают разброс внутри среды, а не между ОС. Что сделано: - `golden:capture` отказывается до запуска браузера, в тексте отказа готовая команда для WSL; - `golden:verify` остаётся законным в любой среде: он ничего не принимает, а как грубая проверка полезен; - обе приёмки (golden и документации) отказываются в чужой среде; - к отказу свидетелей приписывается фраза про расхождение среды — та самая, которой не хватало, чтобы отказ не читался как «объяви больше сцен»; - платформа уезжает в манифесты рядом с версией Chromium: у кадров появился провенанс среды; - осознанный обход есть и требует причину: HP_ALLOW_FOREIGN_CAPTURE. Главный урок задачи — про цену правки файла, у которого записан хеш. Первая редакция встроила проверку в `demo/docs/capture.mjs`, и гейт документации сразу покраснел: его sha записан в индексе скриншотов, и `scripts/check-docs.mjs` их сверяет. То есть проверка, которая ничего не рисует, стоила бы пересъёмки всех картинок документации и визуальной приёмки владельца. Поэтому для документации отказ живёт шагом раньше — `npm run docs:capture` вызывает `scripts/assert-capture-env.mjs` — и шагом позже, на приёмке. По той же причине гейт golden стоит в `demo/golden/policy.mjs`, а не в `run.mjs`: последний входит в корпус sourceFingerprint. Тест закрепляет обе границы: гейт обязан быть в policy.mjs и в npm-скрипте и обязан отсутствовать в двух фингерпринтуемых файлах. Платформа в юнитах — параметр, а не `process.platform`: иначе тест был бы зелёным на Linux и красным на машине владельца, то есть тестом про хост, а не про правило. AGENTS.md приведён к состоянию после #401 (принимается любая среда, доказавшая себя байтовым совпадением непринятых кадров) и разводит проверку и съёмку — прежний текст сливал их в «advisory» и утверждал «accepted only on a complete Linux CI artefact». Свидетели, все проверены отрицательным прогоном: снятый гейт съёмки, гейт, отказывающий и на verify, обход без причины, отказ без команды, убранная приписка про среду, отцепленные гейты обеих приёмок, переставшая бросать обёртка, npm-скрипт без проверки и возврат гейта в каждый из двух фингерпринтуемых файлов. Проверка подключения сначала смотрела только на импорт модуля и молча проходила, когда отказ заменяли на `void` — теперь она проверяет вызов бросающей обёртки. Гейты: npm test 1918 tests, 1917 pass, 0 fail; typecheck зелёный; check-docs зелёный (индекс скриншотов не задет); pytest без HA 378 passed, 3 skipped. Issue: #455 User-Visible: no
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* #455: среда съёмки как проверяемое условие.
|
||||
*
|
||||
* Вопрос владельца был «зачем агенты снимают PNG на Windows, если мы их не
|
||||
* принимаем». Ответ оказался «потому что этому ничто не мешало»: ни один из
|
||||
* шести скриптов съёмки и приёмки не знал, на какой он ОС, а отказ приёмки
|
||||
* говорил про число сцен-свидетелей — то есть подсказывал неверный вывод
|
||||
* «надо объявить больше сцен».
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
ALLOW_FOREIGN_ENV, CAPTURE_CANON_PLATFORM, assertCaptureEnvironment, captureEnvironment,
|
||||
environmentNote, foreignCaptureAllowance, foreignCaptureRefusal,
|
||||
} from '../scripts/capture-environment.mjs';
|
||||
|
||||
test('#455 канон среды — linux, и чужая платформа получает отказ с командой', () => {
|
||||
assert.equal(CAPTURE_CANON_PLATFORM, 'linux');
|
||||
assert.equal(foreignCaptureRefusal({ platform: 'linux' }).refusal, null);
|
||||
for (const platform of ['win32', 'darwin', '']) {
|
||||
const { refusal } = foreignCaptureRefusal({ platform });
|
||||
assert.ok(refusal, `${platform} обязан получить отказ`);
|
||||
assert.match(refusal, /wsl -d Ubuntu/, 'отказ обязан называть команду, а не только запрет');
|
||||
assert.match(refusal, /golden:verify/, 'диагностика законна — это должно быть сказано');
|
||||
assert.match(refusal, new RegExp(ALLOW_FOREIGN_ENV), 'осознанный обход должен быть назван');
|
||||
}
|
||||
});
|
||||
|
||||
test('#455 текст отказа различает съёмку и приёмку и называет предмет', () => {
|
||||
assert.match(foreignCaptureRefusal({ platform: 'win32', stage: 'capture' }).refusal,
|
||||
/съёмка отказана: эталоны golden/);
|
||||
assert.match(foreignCaptureRefusal({ platform: 'win32', stage: 'accept' }).refusal,
|
||||
/приёмка отказана: эталоны golden/);
|
||||
assert.match(foreignCaptureRefusal({ platform: 'win32', kind: 'docs' }).refusal,
|
||||
/скриншоты документации/);
|
||||
assert.match(foreignCaptureRefusal({ platform: 'win32', kind: 'docs' }).refusal,
|
||||
/demo\/docs\/capture\.mjs/, 'у документации своя команда');
|
||||
});
|
||||
|
||||
test('#455 обход требует причину, пустая строка причиной не считается', () => {
|
||||
assert.equal(foreignCaptureAllowance({}), null);
|
||||
assert.equal(foreignCaptureAllowance({ [ALLOW_FOREIGN_ENV]: ' ' }), null);
|
||||
assert.equal(foreignCaptureAllowance({ [ALLOW_FOREIGN_ENV]: ' нет WSL ' }), 'нет WSL');
|
||||
const allowed = foreignCaptureRefusal({ platform: 'win32', allowance: 'нет WSL' });
|
||||
assert.equal(allowed.refusal, null);
|
||||
assert.equal(allowed.allowance, 'нет WSL', 'причина обязана доехать до вывода');
|
||||
assert.ok(foreignCaptureRefusal({ platform: 'win32', allowance: null }).refusal);
|
||||
});
|
||||
|
||||
test('#455 приписка про среду появляется только при расхождении', () => {
|
||||
// Именно этой фразы не хватало отказу «свидетелей 0 из 10».
|
||||
assert.equal(environmentNote({ capturedOn: 'linux', acceptedOn: 'linux' }), null);
|
||||
assert.equal(environmentNote({ capturedOn: 'win32' }), null, 'без второй стороны сравнивать нечего');
|
||||
const note = environmentNote({ capturedOn: 'win32', acceptedOn: 'linux' });
|
||||
assert.match(note, /win32/);
|
||||
assert.match(note, /не в числе объявленных сцен/,
|
||||
'приписка обязана снимать именно неверный вывод про число сцен');
|
||||
});
|
||||
|
||||
test('#455 провенанс среды снимается с процесса, а не выдумывается', () => {
|
||||
const environment = captureEnvironment({ platform: 'win32', arch: 'x64' });
|
||||
assert.deepEqual(environment, { platform: 'win32', arch: 'x64' });
|
||||
assert.equal(typeof captureEnvironment().platform, 'string');
|
||||
});
|
||||
|
||||
test('#455 гейт стоит во всех точках, где кадры появляются и принимаются', () => {
|
||||
// Проверка вида «файл упоминает модуль» слабая, но здесь она про ПОДКЛЮЧЕНИЕ,
|
||||
// а поведение каждой точки закреплено отдельно: golden-policy.test.mjs
|
||||
// (съёмка golden), docs-accept.test.mjs (приёмка документации) и прогон CI
|
||||
// (сами скрипты). Без этой проверки точку можно тихо отцепить.
|
||||
const source = (relative) =>
|
||||
readFileSync(fileURLToPath(new URL(`../${relative}`, import.meta.url)), 'utf8');
|
||||
for (const file of [
|
||||
'demo/golden/policy.mjs', 'demo/golden/accept.mjs',
|
||||
'scripts/docs-accept.mjs', 'scripts/assert-capture-env.mjs',
|
||||
]) {
|
||||
assert.match(source(file), /capture-environment\.mjs/, `${file} не подключает гейт среды`);
|
||||
}
|
||||
// Импорта мало: первая редакция этого теста проходила, когда отказ в
|
||||
// accept.mjs заменили на `void foreign` — модуль остался подключён, а гейт
|
||||
// перестал существовать. Поэтому проверяется форма ОСТАНОВКИ, а не наличие
|
||||
// строки: поведение точек, которые можно запустить на Linux, закреплено в
|
||||
// golden-policy.test.mjs и docs-accept.test.mjs, а эти две проверяются на
|
||||
// Windows и в CI прогоном самих скриптов.
|
||||
// Вызывается именно бросающая обёртка: она останавливает прогон по
|
||||
// построению, поэтому «понизить отказ до печати» без правки этой строки
|
||||
// нельзя. Первая редакция проверки смотрела лишь на импорт модуля — и
|
||||
// молча проходила, когда отказ заменили на `void foreign`.
|
||||
assert.match(source('demo/golden/accept.mjs'),
|
||||
/assertCaptureEnvironment\(\{ kind: 'golden', stage: 'accept' \}\)/,
|
||||
'приёмка golden обязана проверять среду бросающей обёрткой');
|
||||
assert.match(source('scripts/docs-accept.mjs'),
|
||||
/assertCaptureEnvironment\(\{ kind: 'docs', stage: 'accept' \}\)/,
|
||||
'приёмка документации обязана проверять среду');
|
||||
// Съёмка документации гейтится шагом раньше, в npm-скрипте: править
|
||||
// demo/docs/capture.mjs дорого — его sha записан в индексе скриншотов, и
|
||||
// любая правка требует пересъёмки всех картинок (проверено, гейт
|
||||
// документации покраснел сразу). Отсюда требование к самому скрипту:
|
||||
assert.doesNotMatch(source('demo/docs/capture.mjs'), /capture-environment\.mjs/,
|
||||
'demo/docs/capture.mjs трогать нельзя: его sha в индексе скриншотов');
|
||||
const packageJson = JSON.parse(source('package.json'));
|
||||
assert.match(packageJson.scripts['docs:capture'] || '',
|
||||
/assert-capture-env\.mjs docs/,
|
||||
'у съёмки документации обязан быть npm-скрипт с проверкой среды');
|
||||
// `run.mjs` трогать нельзя: он в корпусе sourceFingerprint, и его правка
|
||||
// объявила бы устаревшими бандл, скриншоты документации и индекс эталонов.
|
||||
assert.doesNotMatch(source('demo/golden/run.mjs'), /capture-environment\.mjs/,
|
||||
'гейт обязан стоять в policy.mjs, а не в фингерпринтуемом run.mjs');
|
||||
});
|
||||
|
||||
test('#455 бросающая обёртка бросает и возвращает разрешённую причину', () => {
|
||||
assert.throws(() => assertCaptureEnvironment({ platform: 'win32', allowance: null }),
|
||||
/съёмка отказана/);
|
||||
assert.equal(assertCaptureEnvironment({ platform: 'linux', allowance: null }), null);
|
||||
assert.equal(assertCaptureEnvironment({ platform: 'win32', allowance: 'нет WSL' }), 'нет WSL');
|
||||
});
|
||||
@@ -192,3 +192,18 @@ test('#421 fingerprint-only refresh preserves the complete previous acceptance t
|
||||
});
|
||||
assert.deepEqual(firstRefresh.acceptance, { lastWriteWasFingerprintOnly: true });
|
||||
});
|
||||
|
||||
test('#455 принятый манифест несёт среду приёмки', () => {
|
||||
// Платформу СЪЁМКИ манифест кандидата не несёт и не будет: добавить поле —
|
||||
// значит править demo/docs/capture.mjs, чей sha записан в индексе
|
||||
// скриншотов, то есть заплатить пересъёмкой всех картинок за проверку,
|
||||
// которая ничего не рисует (проверено: гейт документации сразу покраснел).
|
||||
// Поэтому среда фиксируется там, где её знают без правок, — на приёмке.
|
||||
const decision = { replace: ['01-view-desktop'], witnesses: 3, floor: 1 };
|
||||
const accepted = acceptedDocsManifest({
|
||||
manifest: candidate(), decision, platform: 'linux',
|
||||
});
|
||||
assert.equal(accepted.acceptedOn, 'linux');
|
||||
const withoutPlatform = acceptedDocsManifest({ manifest: candidate(), decision });
|
||||
assert.ok(!('acceptedOn' in withoutPlatform), 'без платформы поля быть не должно');
|
||||
});
|
||||
|
||||
@@ -27,7 +27,9 @@ test('golden capture fails on runtime errors but permits missing baselines', ()
|
||||
});
|
||||
|
||||
test('golden verification cannot make a partial success claim', () => {
|
||||
assert.doesNotThrow(() => assertGoldenInvocation('capture', 'one-scenario'));
|
||||
// #455: платформа передаётся явно — иначе юнит был бы зелёным на Linux и
|
||||
// красным на машине владельца, то есть тестом про хост, а не про правило.
|
||||
assert.doesNotThrow(() => assertGoldenInvocation('capture', 'one-scenario', { platform: 'linux' }));
|
||||
assert.doesNotThrow(() => assertGoldenInvocation('verify', ''));
|
||||
assert.throws(() => assertGoldenInvocation('verify', 'one-scenario'), /complete matrix/);
|
||||
assert.throws(() => assertGoldenInvocation('unknown', ''), /unknown golden mode/);
|
||||
@@ -309,3 +311,21 @@ test('размер матрицы обязателен: догадываться
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('#455 съёмка golden в чужой среде отказывается заранее, verify — нет', () => {
|
||||
// До этой задачи `golden:capture` на Windows отрабатывал штатно и писал PNG,
|
||||
// а стена появлялась на приёмке. Отказ переехал на первую строку прогона.
|
||||
assert.throws(() => assertGoldenInvocation('capture', '', { platform: 'win32' }),
|
||||
/съёмка отказана/);
|
||||
assert.throws(() => assertGoldenInvocation('capture', '', { platform: 'win32' }),
|
||||
/wsl -d Ubuntu/);
|
||||
// Диагностика законна и остаётся доступной в любой среде.
|
||||
assert.doesNotThrow(() => assertGoldenInvocation('verify', '', { platform: 'win32' }));
|
||||
// Осознанный обход существует и требует причину.
|
||||
assert.doesNotThrow(() => assertGoldenInvocation('capture', '', {
|
||||
platform: 'win32', allowance: 'на этой машине нет WSL',
|
||||
}));
|
||||
assert.throws(() => assertGoldenInvocation('capture', '', {
|
||||
platform: 'win32', allowance: '',
|
||||
}), /съёмка отказана/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user