mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Аудит 14.09, §4 и §9. `golden-report.json` не нёс платформу вовсе, поэтому приёмщик вызывал `captureEnvironment()` у себя и записывал СВОЮ платформу как платформу кадров. На `ad4000f9` это дало `"platform": "win32"` у кадров, снятых Linux-прогоном 34853080375, а причина осознанного обхода осталась в stdout и в индекс не попала — хотя `AGENTS.md` обещает след в обоих местах. Сами PNG были и остаются целы: 101 свидетель совпал байт-в-байт. Врал провенанс. Что сделано: - отчёт съёмки получил раздел `capture`: платформа, архитектура, сборка Chromium, отпечаток материала и — в CI — прогон с попыткой и SHA. Схема отчёта поднята до 2; - приёмка читает среду съёмки из отчёта. Гейт чужой среды теперь судит обе стороны: съёмку (из отчёта) и приёмку (свою). Отказ — до единой записи; - индекс эталонов поднят до схемы 2 и различает `capturedOn` и `acceptedOn`, несёт раздел `capture` и причину осознанного обхода в `foreignCapture`; - отчёт схемы 1 платформы съёмки не несёт физически: это отдельная явная ветка, `capturedOn` уезжает `null`. Выдумывать платформу нельзя — ровно этим задача и вызвана; - `--baselines=<dir>` у приёмки: без него проверить «отказ произошёл ДО записи» можно было бы только порчей рабочего дерева, то есть никак (#556). Осознанно отменено решение #455 «не трогать run.mjs»: ради ГЕЙТА цена фингерпринта не окупалась, ради ПРОВЕНАНСА окупилась — платформу кадров знает только тот, кто их снял. Плата разовая: пересобран бандл, индекс скриншотов документации переснят отдельным коммитом. Тест #455 переписан под новый инвариант, а не удалён. Свидетели: `test/golden-capture-provenance.test.mjs` — семь проверок, все пять сценариев приёмки из issue, включая «отказ до записи» и «подмена PNG и неполный артефакт по-прежнему fail-closed». Мутанты `golden-index-invents-capture-platform` (возвращает платформу приёмщика) и `golden-report-provenance-optional` (разрешает отчёт схемы 2 без провенанса) прогнаны лично: оба краснеют. npm test 2762/2761/0 fail, typecheck чистый. Issue: #571 User-Visible: no
130 lines
9.9 KiB
JavaScript
130 lines
9.9 KiB
JavaScript
/**
|
||
* #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`.
|
||
// #571 заменил бросающую обёртку на явную пару проверок: судить надо ДВЕ
|
||
// среды — съёмки (из отчёта) и приёмки (своя), а обёртка знала только свою.
|
||
// Форма остановки прежняя: `throw`, а не печать.
|
||
assert.match(source('demo/golden/accept.mjs'),
|
||
/for \(const \[platform, stageNote\] of \[[\s\S]*?\]\) \{[\s\S]*?if \(refusal\) throw new Error\(/,
|
||
'приёмка golden обязана отказывать по обеим средам, а не печатать');
|
||
assert.match(source('demo/golden/accept.mjs'), /reportCaptureProvenance\(report\)/,
|
||
'среда съёмки обязана читаться из отчёта, а не из process.platform приёмщика');
|
||
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-скрипт с проверкой среды');
|
||
// До #571 `run.mjs` не трогали принципиально: он в корпусе sourceFingerprint,
|
||
// и правка объявляет устаревшими бандл и индекс скриншотов. Ради ГЕЙТА эта
|
||
// цена не окупалась — он прекрасно живёт в policy.mjs. Ради ПРОВЕНАНСА
|
||
// окупилась: платформу кадров знает только тот, кто их снял, и записать её
|
||
// больше негде. Цена заплачена один раз, осознанно (#571).
|
||
assert.doesNotMatch(source('demo/golden/run.mjs'), /assertGoldenInvocation\([^)]*platform/,
|
||
'гейт среды остаётся в policy.mjs: в run.mjs только сбор провенанса');
|
||
assert.match(source('demo/golden/run.mjs'), /capture: captureProvenance\(\{ chromium, buildFingerprint \}\)/,
|
||
'отчёт съёмки обязан нести провенанс среды (#571)');
|
||
});
|
||
|
||
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');
|
||
});
|