Files
houseplan-card/demo/golden
Claudeandclaude[bot] b998b0b34a feat(moon): the moon with any background, and its status in General settings (#718)
The owner decided on 30.09 that the moon is not part of the "Follow the Sun"
environment but a switch of its own: with a static background (global or a
space's own) the card showed no moon even with the switch on, and the switch
said nothing about why the moon was missing right now.

With a static background there is no environment, so the moon stands in its
own layer, `.hp-moon-sky`: the first child of `.stage` / `.hp-static-stage`,
the whole scene, no z-index, filter or will-change, under the plan by DOM
order, fading with the #101 View weight. Inside is the very #661 element, so
place, size, art and fades are unchanged, and a background switch moves it to
its new parent in the same render without a flicker. The phase comes from the
same `resolveDayCycle`, computed only while the moon is on and on View; without
`sun.sun` both cards keep their 30 s clock ticker and re-render only when the
phase changes (the environment is still compared by its whole fingerprint).

General settings get a second caption line under the moon switch
(`data-moon-status`): one snapshot per opening, judged by the lazy chunk as if
the switch were on, first reason wins (no home, day, below 3°, under 3 %),
numbers rounded and clamped below the threshold they missed. `moonStatus`
decides "shown" with the same `moonShownAt` as the element. It lives in a
WeakMap beside the draft, so it never makes the dialog dirty; a closed
opening's result is dropped. The dialog loads the chunk through the gate's
loader (`withMoon`), now shared by every caller while a load is in flight, so
there is still one fingerprint check and one retry token.

Bundle (same build, against origin/dev): initial View 300 072 -> 300 248 B gzip
(+176 B, under the 500 B of the spec; budget and ceiling not raised); lazy
editor 238 558 -> 238 991 B (+433 B, the line and English strings); lazy moon
11 385 -> 11 712 B (+327 B, layer CSS and status). `src/moon.ts` stays out of
the initial and the editor graph; bundle-budget now refuses an editor/moon
overlap. Monolith metrics: hostRefs 4 885 -> 4 888 — the three `host.` reads of
`src/editors/moon-status.ts` (hass, `_settingsDialog`, requestUpdate) through
its own three-member interface, not the editor port; the other five metrics
are unchanged. houseplan-editor-runtime.ts grows by two lines (import, call).

Tests: AC9/AC10/AC15 and the sky layer in test/moon.test.mjs (the #661
"static -> nothing" check inverted), AC14 and the opening lifecycle in
test/moon-settings.test.mjs, smokes demo/smoke_moon_static.mjs (AC1-AC6; AC1
and AC3 were red on dev) and demo/smoke_moon_status.mjs (AC11/AC12), AC7 in
smoke_daycycle_layer_budget. Golden: two new scenes
(static-bg-moon-gibbous-white-light, static-bg-moon-crescent-south-dark,
matrix v70), the harness checks the moon's parent by background and waits for
the status line in the General settings frames. Four new mutants; the clock
ticker one is a browser guard (201 at the guideline of 200).

Issue: #718
User-Visible: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-10-01 05:25:21 +00: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, current source fingerprint and one explicit source of provenance: a GitHub Linux capture or the self-hashed WSL passport described below. 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).

Displayed version (#512)

Frames never show the real CARD_VERSION. The harness sets the test seam window.__HP_VERSION_OVERRIDE__ = '0.0.0-golden' before any card is created and feeds the same constant wherever it plays the backend (integration_version, support facts), so the about dialog, the version-recovery banner and the support and backup previews print 0.0.0-golden in every baseline. A beta bump therefore changes no golden frame; the version-mismatch scenarios keep their own 0.0.0-golden-backend and still exercise the frontend≠backend relation. The product never sets the seam (src/card-version.ts).

Diagnostic 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/. A plain local capture is diagnostic and cannot be accepted. To update references use one of the two reviewed-source workflows below.

The CI-artifact path remains unchanged. Download and unpack the complete golden-images artifact of a Validate run, review every declared frame, then:

npm run golden:accept -- --reviewed --from=<unpacked-golden-images> \
  --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.

Attested WSL acceptance with one GitHub round trip (#641)

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 only through the repository's WSL/ext4 clone, because admissibility is now proved rather than assumed. Publish the named issue branch first and leave its worktree clean. The command refuses native Windows, /mnt/c, a detached/dirty/unpublished SHA, pin drift and a stale build; it then captures the complete current matrix and refuses undeclared changes, missing frames or an insufficient byte-identical witness floor:

cd ~/houseplan-card
git fetch origin
git switch issue/<NN>-<slug>
git pull --ff-only origin issue/<NN>-<slug>
npm run golden:wsl:capture -- --expect-change=<the scenarios you changed>

# Review artifacts/golden/actual and diff, then use exactly the same intent.
npm run golden:accept -- --reviewed --from=artifacts/golden \
  --expect-change=<the scenarios you changed>
npm run golden:verify

The capture writes artifacts/golden/wsl-attestation.json. Its self-hash binds repository/branch/commit/tree and remote SHA, WSL distro/kernel/architecture and filesystem, Node/npm/Playwright/Chromium identity, package-lock.json, source fingerprint, matrix version, every scene's dimensions and PNG checksum, the report checksum, witness count/floor and the declared acceptance intent. The accept command verifies the same facts again before writing and stores the local provenance separately in baselines-index.json; it never pretends that a local capture came from GitHub Actions.

The command prints the exact terminal trailer for the baseline commit:

Release: vX.Y.Z-beta.N
Baseline-Reviewed-Local: sha256:<wsl-attestation hash>

Use either that local trailer or the existing Baseline-Reviewed: <GitHub run URL>, never both. The local digest must match baselines-index.json.localAttestation.sha256, which the commit hook and CI provenance job verify. Push the accepted baseline commit and wait for a full GitHub Validate on that exact SHA before S7/merge/release. That final run captures and checks the matrix independently; the WSL path removes only the earlier expected-red artifact-transport run.

scripts/golden-container.mjs and plain golden:capture remain useful local diagnostics, but their output has no WSL attestation and therefore cannot be accepted. If the environment is not pixel-equivalent, unrelated scenarios come out different anyway; a mismatched font set or browser can only fail.

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.