Files
houseplan-card/demo/golden
Claude 28a4cb5eec 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
2026-09-04 19:26:35 +03:00
..

HP-QA-01 golden images

This layer catches visual regressions that DOM smokes cannot: wall seams and end caps, thick opening tunnels, Glow/sun clipping, hover contours, editor chrome, the open contextual tray at wide/medium/narrow widths in English and Russian (selection, tool options, group and palette), long dialog titles/footers, mobile clipping, themes and zoom/remount. The desktop and mobile device-dialog scenarios use a real light and make the complete source-role, Glow colour, brightness and radius controls visible; capturing only the top of that section fails the scenario before comparison. The Glow matrix also keeps one deliberately opaque custom-fill scene with a single source and two doorways: it makes hard spill wedges and fully unlit radial spokes visible instead of hiding them under a translucent room fill.

Safety contract

  • A build fingerprint embedded by Rollup must match src/, Rollup/TypeScript configuration and locked package inputs; stale committed demo bundles fail before the first screenshot.
  • Chromium, viewport, locale, timezone, colour profile, font rendering, animations and caret are controlled by the runner.
  • capture writes only to ignored artifacts/golden/; it never changes a baseline and never claims a missing baseline passed. Any scenario runtime error makes capture fail, including the initial no-baseline CI run.
  • verify requires every image plus a matching matrix manifest and fails on missing/different/error scenarios, browser mismatch or a baseline whose hash no longer matches the reviewed manifest.
  • accept requires --reviewed, a complete candidate report and current source fingerprint. It validates the whole set before copying anything and is the only command allowed to update baselines.
  • scripts/golden-accept.mjs wraps accept and additionally requires the reviewer to declare intent, with two separate flags because they assert two different things. --expect-change=<id,id> means "I know why this existing baseline moved"; --expect-new=<id,id> means "I have looked at this new frame". Anything that differs, or arrives without a baseline, and is not named refuses the whole acceptance before a single file is copied; naming a scenario under the wrong flag refuses it too. Only the named scenarios are written: everything else keeps its reviewed bytes and its manifest hash, because passed means "within threshold", not "byte-identical", and copying every candidate let sub-threshold drift ratchet the baselines to the newest environment unseen (#351). The first flag is what makes a local capture admissible (see below) and blocks the one-command "accept everything so CI turns green"; the second stops an empty or clipped frame from becoming the contract unseen (#350).

Workflow

Build and copy the exact current source first:

npm run build
npm run bundle:sync
npm run golden:capture

Review artifacts/golden/actual/ and, when existing references are present, artifacts/golden/diff/. If every image is intentional:

node scripts/golden-accept.mjs --reviewed --expect-change=wall-junctions-plan-t-dark
npm run golden:verify

Never accept images merely to make CI green. A matrix/framing change increments GOLDEN_MATRIX_VERSION; a normal rendering fix does not. The first canonical Linux baseline was reviewed and accepted during the v1.60.3-beta.1 gate.

Capturing candidates without a second CI round trip (#334)

Accepting from the golden-images CI artifact still works and is still the safest route: unpack it and pass --from=.... It costs two full CI runs per visual fix, though — one to produce the artifact and one to verify the accepted baseline — and at matrix version 48 that toll is paid often.

A local capture is admissible instead, because admissibility is now proved rather than assumed. Desktop font rasterisation can differ from the runner, but it cannot differ quietly: it would move every text-bearing scenario, not only the ones under edit. So the rule is simply that the capture must reproduce every accepted baseline the reviewer did not intend to change:

node scripts/golden-container.mjs                      # capture in the pinned image
node scripts/golden-accept.mjs --reviewed --expect-change=<the scenarios you changed>

If the environment is not pixel-equivalent, unrelated scenarios come out different, the wrapper names them and refuses. A wrong container tag or a mismatched font set therefore cannot corrupt baselines — it can only fail.

scripts/golden-container.mjs derives the image tag from the playwright version locked in package-lock.json, so the container Chromium equals the runner's; --image= overrides it when a distro-specific tag is needed (...:v1.62.0-jammy). The host node_modules is shadowed by an anonymous volume: the repository copy may be built for Windows, and npm ci inside the container would otherwise replace it with Linux binaries. The run does write dist/ and the three bundle copies, exactly as a local npm run bundle:sync would.

Docker is not a requirement of the rule, only a convenience: any Linux environment that satisfies the parity condition qualifies, WSL included. The chromium string recorded in the manifest keeps the browser build itself pinned, and golden:verify rejects a manifest captured by a different build.

scripts/golden-accept.mjs deliberately wraps demo/golden/accept.mjs instead of replacing its checks: every .mjs under demo/golden belongs to sourceFingerprint, so editing the acceptance tool itself would declare the committed bundle, the documentation screenshot manifest and the baseline manifest stale — the very double round trip this change removes. Narrowing that corpus is worthwhile but separate: scripts/source-fingerprint.mjs is itself a build input, so any change to it forces one bundle rebuild.

Scenarios may also declare a semantic pixel region (for example, a receiving room that must contain warm light). golden:capture and golden:verify reject the capture before baseline comparison when that visual precondition is empty; a reviewed but meaningless PNG therefore cannot become the contract.