Files
houseplan-card/docs/testing-notes/mutation-browser-guards.md
T
Matyshandclaude[bot] 3e98ac4dde fix(test): include late page errors in smoke teardown verdict
Count delivered page errors after awaited browser close. Exercise the public
harness lifecycle with deterministic negative subprocess cases and retain the
unchanged Chromium probes for the real transport. Move the premature-snapshot
mutation witness to the deterministic Node suite.

Issue: #776
User-Visible: no
2026-10-06 14:06:44 +00:00

363 lines
16 KiB
Markdown

# Browser guards mutation registry (#659)
This is the reviewed classification of every mutation witness that still needs a browser.
The guideline is `200` (#699: a guideline, not a wall); `mutation-gate --check`,
`npm run inventory` and the unit contract all read this same inventory. A new browser guard
must be added deliberately under one reason below, and its mutant `because` must explain
the concrete browser-only invariant.
`test/mutation-gate.test.mjs` checks each category count against its listed IDs,
the total against the browser guards in `MUTANTS`, and both guideline numbers
against `BROWSER_GUARD_LIMIT` (#767). Category names must agree between the table
and inventory; each current browser guard ID must occur exactly once.
Converted witnesses are not listed here: their registry guard names an explicit `node --test`
suite. The original #659 conversions also use `test/mutation-browser-offload.test.mjs`;
new behavioral witnesses execute the consumer directly where possible. Catching the mutant
is verified by the nightly run under PROCESS §2.7, not by a development-time mutation run.
`led-unbound-in-view` moved to `test/led-strip-runtime.test.mjs` in #791: its guard
requires a non-null marker absent from runtime devices, whereas the browser's
`marker: null` fixture exits before the mutated guard. The Node witness checks the
exact surviving strip/owner and its live light; the browser smoke still covers
unbound-strip visibility and cold loading.
`glow-static-led-release-skipped` moved to `test/space-card-led-disconnect.test.mjs`
in #794: the real static-card callback must release populated LED state
synchronously, before a detached render can hide missing teardown. A
connection-only negative case suppresses late updates but retains its old
lifecycle; the browser smoke retains visible fade coverage without claiming
that transition events alone prove disposal.
| Category | Count | Why a browser is still required |
| --- | ---: | --- |
| Performance threshold | 4 | The witness measures real browser wall-time or frame work; a pure assertion cannot prove the budget. |
| Browser harness integrity | 3 | The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself. |
| Paint, cascade and layer composition | 42 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Pointer geometry and trusted interaction | 50 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
| Responsive DOM layout | 39 | The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size. |
| Custom-element and HA browser lifecycle | 104 | The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition. |
| **Total** | **242 / 200** | Above the guideline `mutation-gate --check` warns rather than fails (#699); each guard above it is held by its own reason in this inventory and its `because`. |
## Measured effect
The measurements below are GitHub-hosted Linux runs from 2026-09-27. They are
recorded separately from the policy limit: the limit proves that browser guards
cannot grow unnoticed, while timings show the effect actually observed on the
shared runners.
| Measurement | Before | After | Observed change |
| --- | ---: | ---: | ---: |
| Full-registry wall time | [54:18](https://github.com/Matysh/houseplan-card/actions/runs/36298676826) (1,017 mutants) | [48:08](https://github.com/Matysh/houseplan-card/actions/runs/36316263355) (1,028 mutants) | **-6:10 (-11.4%)**, despite 11 additional mutants |
| Sum of the six `Each test catches its breakage` steps | 4:17:19 | 4:12:32 | **-4:47 (-1.9%)** |
| Longest full-registry shard step | 50:45 | 45:13 | **-5:32 (-10.9%)** |
| Same converted-witness slice, aggregate time | 33:43.9 | 12:07.7 | **-21:36.2 (-64.0%)** |
| Same converted-witness slice, critical shard | 8:05.1 | 2:19.3 | **-5:45.8 (-71.3%)** |
The candidate slice compares the same 83 converted mutant ids in the baseline
full-registry log and in the S7 candidate Validate
[36316464432](https://github.com/Matysh/houseplan-card/actions/runs/36316464432).
It sums the interval between consecutive per-mutant completion timestamps on
each of the six shards. One of the 84 converted ids, `active-tab-not-revealed`,
was the first completed mutant in its shard, so the log cannot isolate its time
from clean-guard setup and it is excluded from both sides. The complete candidate
run, which also selected registry/tooling mutants outside this converted slice,
finished in 10:03.
The original estimate of three to four hours saved on a full-registry run is not
supported by this A/B evidence. The directly converted slice is materially
faster, but registry growth and shared-runner variance consume most of that gain
in the end-to-end total; only the measured reductions above are claimed.
## Reviewed per-mutant inventory
### Performance threshold
The witness measures real browser wall-time or frame work; a pure assertion cannot prove the budget.
- `junction-limit-p3-quadratic-again`
- `junction-limit-p4-bruteforce-again`
- `wall-draw-full-preflight-again`
- `wall-draw-wall-artifact-discarded`
### Browser harness integrity
The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself.
- `benchmark-page-verdict-unwatched`
- `report-page-errors-skips-round-trip`
- `smoke-guard-forgets-to-register-pages`
`smoke-guard-blind-to-tail` moved to `test/smoke-harness-lifecycle.test.mjs`
in #776: controlled delivery during awaited close deterministically rejects a
premature counter snapshot. The unchanged Chromium probes still check the
real Playwright page-error channel.
### Paint, cascade and layer composition
- `battery-icon-collapses-glyph`
- `battery-zigbee-captions-under-endpoint`
The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium.
The #798 Zigbee witnesses exercise the mounted SVG for local and parent-bubble
routes in both themes and at minimum/default/maximum plan zoom. Unknown-LQI
outlines must scale with the plan, including live CSS projection; known-LQI
parent routes must retain the same line/arrow color as local routes.
- `daycycle-outline-not-promoted`
- `daycycle-outline-promoted-on-inner-paper`
- `daycycle-static-outline-promoted-on-inner-paper`
- `decor-restored-below-room-fills`
- `device-keyboard-bypasses-click-path`
- `device-long-value-ellipsis-restored`
- `device-unavailable-hover-restored`
- `glow-entry-initial-opacity-skipped`
- `golden-filled-tunnel-removed`
- `golden-lamp-out-of-reach`
- `hatch-static-renderer-untouched`
- `hatch-stroke-not-scaled`
- `hatch-zoom-compensation-back`
- `iso-first-frame-reveals-flat-during-lazy-load`
- `iso-room-label-44-box-centres-name`
- `iso-sun-card-drops-occluders`
- `iso-sun-flat-wedges-remain`
- `iso-theme-dark-wall-rule-returns`
- `led-badge-dropped`
- `led-core-white-under-glow`
- `led-zoom-coarse-midpoint-wrong`
- `led-field-endpoint-dropped`
- `led-field-disc-cancels-fan`
- `led-field-compound-clip-children`
- `led-field-circle-events-missing`
- `led-icon-not-suppressed`
- `led-source-stays-round-at-anchor`
- `led-static-live-ignored`
- `stage3-w4-device-target-loses-44px-floor`
- `stage3-w5-runtime-nudge-writes-storage`
- `stage3-w6-no-borders-keeps-raised-plates`
- `stage3-w7-sun-state-enters-structural-key`
- `sun-ray-origin-cache-ignored`
- `sun-ray-origin-save-forced-inner`
- `value-static-icon-keeps-live-vacuum`
- `value-static-icon-keeps-route-warning`
- `value-static-icon-keeps-vacuum-overlay`
- `zigbee-topology-parent-lqi-discarded`
- `zigbee-topology-unknown-arrow-outline-removed`
- `zigbee-topology-unknown-outline-ignores-plan-zoom`
### Pointer geometry and trusted interaction
- `battery-passive-frame-intercepts-pointer`
The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry.
- `align-guides-exclude-dead-source`
- `align-point-reads-frozen-snapshot`
- `decor-default-style-debounce-cut`
- `decor-default-style-seed-cut`
- `decor-keyboard-nudge-drops-focus-dialog-guards`
- `decor-keyboard-nudge-reruns-magnet`
- `dense-device-hit-browser-skips-painted-priority`
- `double-fit-bypasses-canonical-fit-all`
- `furniture-art-editor-adopt-skipped`
- `furniture-edge-handles-steal-the-corner`
- `furniture-exterior-surface-removed`
- `furniture-shift-listeners-not-attached`
- `furniture-wall-runtime-drops-drag-side`
- `furniture-wall-runtime-drops-raw-intent`
- `led-pan-adds-point`
- `led-pinch-calls-action`
- `led-zoom-quality-never-coarse`
- `led-zoom-noop-clears-lease`
- `live-pinch-compositor-demoted-on-active-lit-commit`
- `opening-dimension-overlay-hidden`
- `opening-search-hides-none`
- `opening-search-select-not-wired`
- `placement-accepts-any-mouse-button`
- `reorder-skips-materialization`
- `resize-history-boundary-repair-removed`
- `resize-label-uses-old-room-gear-centre`
- `resize-labels-hide-narrow-area`
- `resize-pointer-capture-removed`
- `resize-preview-reject-silent`
- `room-fit-html-overlay-jumps-ahead`
- `room-fit-pan-release-reaccepted`
- `room-fit-persists-zoom`
- `safe-resize-commit-preflight-bypassed`
- `sections-editor-transition-uses-window-height`
- `sections-grid-drops-stage-flex-chain`
- `sections-grid-falls-back-to-viewport-height`
- `sections-resize-drops-stage-refit-observer`
- `space-card-decor-capability-change-not-adopted`
- `space-card-decor-capability-downgrade-does-not-clear-assets`
- `tab-drag-outlives-the-card`
- `tab-drag-survives-release-outside`
- `tab-drag-target-follows-captured-source`
- `tab-drop-indicator-always-before`
- `tab-drop-outside-commits-last-target`
- `tab-reorder-not-persisted`
- `touch-pinch-marker-hold-rearmed`
- `touch-pinch-zoom-persists-per-frame`
- `unavailable-toggle-stays-silent`
- `wall-draw-rejection-rollback-skipped`
### Responsive DOM layout
- `zigbee-tooltip-collision-bypass`
The invariant depends on measured element boxes, responsive breakpoints, native/HA dialog shells or focusable target size.
- `dialog-ha-disconnected-reject-reopen-enabled`
- `dialog-ha-rejected-close-reopen-disabled`
- `dialog-native-reconnect-recovery-disabled`
- `dialog-native-surface-stretches-to-viewport`
- `dialog-native-update-recovery-disabled`
- `fit-house-hidden-walls-vote`
- `grid-scale-imperial-roundtrip-drift`
- `grid-scale-opening-hit-unscaled`
- `grid-scale-opening-symbol-unscaled`
- `grid-scale-plan-chrome-unscaled`
- `grid-scale-static-factor-missing`
- `header-menu-item-below-44`
- `header-menu-outside-tap-reaches-plan`
- `hidden-room-names-compact-svg-fallback`
- `hidden-room-names-full-svg-fallback`
- `hidden-room-names-iso-override`
- `hp-dialog-ignores-flex-content`
- `panel-readonly-empty-bypasses-write-capability`
- `phone-editor-close-hidden-with-mode-buttons`
- `phone-header-at-tablet-width`
- `phone-header-shows-title`
- `phone-header-wraps`
- `static-card-descendants-hit-testable`
- `summary-dialog-drops-flex-content`
- `summary-picker-hot-add-stays-filtered`
- `tab-editing-without-write-access`
- `toolbar-active-highlight-stops-at-tab`
- `toolbar-close-back-inside-tab`
- `toolbar-close-hidden-at-medium-width`
- `toolbar-close-slot-collapses-outside-editor`
- `toolbar-close-slot-idle-focusable`
- `toolbar-close-slot-leaves-mode-group`
- `toolbar-close-target-below-24`
- `toolbar-device-count-returns`
- `toolbar-mode-zoom-gap-regresses`
- `warm-dialog-drops-transferred-baseline`
- `warm-resume-collapses-pending-header`
- `warm-memo-publishes-torn-header-stage-pair`
### Custom-element and HA browser lifecycle
`wall-face-lineage-skips-post-reconcile-settlement` (#804) needs the production room
dialog, post-reconciliation runtime wiring, history, asynchronous `config/set` and
reload. Pure lineage-helper tests cannot prove that the editor actually settles
residual-owned IDs before the strict wall-model barrier or rolls rejected writes back.
The invariant crosses Lit/custom-element lifecycle, browser storage/events, lazy loading or a complete HA-card state transition.
- `accepted-marker-rolled-back-by-layout-failure`
- `area-relocation-clears-whole-history`
- `area-relocation-loses-position-on-refusal`
- `backdrop-busy-dismiss-races-decision`
- `backdrop-downscale-drops-alpha`
- `backdrop-phase2-falls-back-to-original`
- `backdrop-probe-always-safe`
- `barrier-cache-never-invalidated`
- `camera-anchor-from-presented`
- `camera-cancel-loses-zoom`
- `child-readd-clears-parent-tombstone`
- `cold-view-toggle-delegated-to-runtime`
- `cold-view-vacuum-mapid-delegated`
- `color-picker-confirm-click-through`
- `color-picker-invalid-confirm-latch-removed`
- `config-updated-event-ignored`
- `confirm-dialog-loses-alertdialog`
- `current-rejected-physical-write-keeps-optimistic-wall`
- `danger-confirm-back-into-the-branch`
- `danger-confirm-lost-space-request-guard-removed`
- `danger-confirm-lost-space-transition-cancel-removed`
- `danger-confirm-uses-last-rendered-language-gate`
- `danger-confirm-warm-language-guard-removed`
- `danger-confirm-warm-transition-cancel-removed`
- `daycycle-programmatic-camera-skips-safe-outline`
- `device-echo-keeps-local-noncanonical`
- `device-focus-tooltip-blur-cleanup-removed`
- `device-focus-tooltip-handler-removed`
- `device-focus-tooltip-room-hover-overwrites`
- `device-inbox-batch-rollback`
- `device-markers-rendered-without-keys`
- `device-pointer-leave-clears-focus-fallback`
- `device-position-cancel-routed-to-commit`
- `discovery-reset-writes-a-copy`
- `editor-neutral-escape-does-not-exit`
- `empty-space-cleanup-disabled`
- `fixed-floor-transition-guard-bypassed`
- `floor-geometry-key-global-epoch`
- `floor-geometry-key-ignores-content`
- `french-locale-wrong-dictionary`
- `glow-barrier-render-pass-wiring-skipped`
- `glow-static-ready-after-disconnect`
- `household-enter-stops-acting`
- `household-marker-drops-keyboard-reach`
- `hp-dialog-escape-does-not-close`
- `junction-limit-baseline-cache-stale`
- `junction-limit-candidate-fail-open`
- `junction-limit-write-gate-removed`
- `led-auto-slot-reserved`
- `led-hidden-marker-loads-chunk`
- `locale-failure-toast-dropped`
- `marker-reject-keeps-optimistic-candidate`
- `marker-rollback-keeps-enqueue-time-revision`
- `moon-static-clock-tick-off`
- `namespace-loader-returns-english`
- `near-axis-authoring-snap-bypassed`
- `onboarding-loader-skips-namespace-ensure`
- `openings-rendered-without-keys`
- `plan-only-preview-label-hidden`
- `plan-room-area-icon-hidden`
- `plan-room-area-icon-navigates`
- `plan-upload-client-limit`
- `plan-upload-guard-original`
- `plan-upload-reduced-over-limit-staged`
- `post-write-tail-runs-on-refused-gate`
- `readonly-view-syncs-new-devices`
- `render-invalidation-renders-irrelevant-ha`
- `render-reconcile-restarts-editor-runtime-cycle`
- `reopened-room-from-registry-space`
- `room-accept-leaves-coincident-partitions`
- `room-gear-second-touch-keeps-drag`
- `room-settings-click-does-not-open`
- `room-tooltip-off-skips-pointer-modality`
- `rooms-rendered-without-keys`
- `rooms-rendered-without-space-key`
- `same-space-room-change-recenters`
- `space-card-rooms-rendered-without-keys`
- `space-create-hidden-display-override`
- `stairs-view-pan-opens-target-floor`
- `stairs-view-tread-lines`
- `support-invalid-response-leaks-issued-token`
- `support-stale-preview-response-revives-consent`
- `support-timeout-claims-success`
- `vacuum-overlay-back-to-the-dock-space-filter`
- `view-current-space-aria-removed`
- `volumetric-kiosk-ignores-setting`
- `wall-face-apply-skips-overlap-guard`
- `wall-face-lineage-skips-post-reconcile-settlement`
- `wallthick-hit-narrowed`
- `warm-late-resume-beats-user-mode`
- `warm-late-resume-crosses-space`
- `warm-pan-during-runtime-keeps-refit-blocked`
- `warm-pending-mode-leaves-revive-waiting`
- `warm-resume-camera-depends-on-dialog`
- `warm-resume-overwrites-view-return-camera`
- `writer-history-skips-finished-chain-normalization`
- `zigbee-topology-endpoint-cleanup-skipped`
- `zigbee-topology-endpoint-elevation-removed`
- `zigbee-topology-hovered-endpoint-elevation-removed`
- `zigbee-topology-overlay-double-live-projection`
- `zigbee-topology-overlay-layer-lowered`
- `zigbee-topology-solid-route-dashed`
- `zigbee-topology-unknown-casing-removed`
- `zigbee-topology-unrelated-markers-raised`