test: restore the missing LED runtime owner witness (#791)

Exercise a non-null marker absent from the render-device roster alongside a
valid lit neighbour. The previous null-marker smoke returned before the
mutated guard, so it never tested this defensive consumer boundary.
Move this witness to the direct Node runtime suite and update its inventory.

Issue: #791
User-Visible: no
This commit is contained in:
Matysh
2026-10-04 17:09:23 +03:00
parent 14f5b90c24
commit a5e06b0527
3 changed files with 44 additions and 7 deletions
+11 -5
View File
@@ -7,18 +7,25 @@ must be added deliberately under one reason below, and its mutant `because` must
the concrete browser-only invariant.
Converted witnesses are not listed here: their registry guard names an explicit `node --test`
suite plus `test/mutation-browser-offload.test.mjs`, and every converted mutant is run once
to prove that the Node witness actually kills it.
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.
| 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 | 4 | The mutation breaks page-error, round-trip or page-registration observation in the browser harness itself. |
| Paint, cascade and layer composition | 38 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Paint, cascade and layer composition | 37 | The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels produced by Chromium. |
| Pointer geometry and trusted interaction | 49 | The invariant depends on hit testing, pointer capture, touch/keyboard dispatch or live DOM geometry. |
| Responsive DOM layout | 38 | 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** | **237 / 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`. |
| **Total** | **236 / 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
@@ -102,7 +109,6 @@ The invariant depends on computed CSS, SVG paint, clipping, stacking or pixels p
- `led-icon-not-suppressed`
- `led-source-stays-round-at-anchor`
- `led-static-live-ignored`
- `led-unbound-in-view`
- `stage3-w4-device-target-loses-44px-floor`
- `stage3-w5-runtime-nudge-writes-storage`
- `stage3-w6-no-borders-keeps-raised-plates`
+4 -2
View File
@@ -14506,8 +14506,10 @@ const MUTANT_DEFINITIONS = [
},
{
id: 'led-unbound-in-view',
guard: 'node demo/smoke_led_strip_glow.mjs',
because: "#780 ТЗ §5: an unbound strip has no View representation, light or target",
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
+ '&& node --test test/led-strip-runtime.test.mjs',
because: '#780 ТЗ §5 / #791: ledFrame skips a non-null marker absent from runtime devices '
+ 'without losing a valid neighbouring strip; the executable frame boundary needs no browser',
patches: [{
file: 'src/led-strip-runtime.ts',
find: " if (!device || device.space !== input.space.id) continue;",
+29
View File
@@ -444,6 +444,35 @@ test('#784/AC9: the frame gives every strip the 30 cm default, not the shared ra
assert.ok(Math.abs(frame.views[0].radius - (30 / 5) * (1000 / 240)) < 1e-9, `radius ${frame.views[0].radius}`);
});
test('#791: the frame skips missing runtime owners without dropping a valid neighbouring strip', () => {
const lamp = device({ entities: ['light.led'], marker: { id: 'm1', binding: 'device:m1' } });
const points = [[0.1, 0.1], [0.4, 0.1]];
const strips = [
// A non-null reference reaches the runtime-owner guard; marker:null alone
// is rejected earlier and never exercised the escaped nightly mutant.
{ id: 'missing-owner', points, marker: 'absent-from-runtime' },
{ id: 'unbound', points, marker: null },
{ id: 'other-space', points, marker: 'm2' },
{ id: 'inactive', points, marker: 'm1', active: false },
{ id: 'valid', points, marker: 'm1' },
];
const before = structuredClone(strips);
const frame = ledFrame({
space: { id: 's', vb: [0, 0, 1000, 1000], rooms: [{ id: 'room' }], led_strips: strips },
devices: [lamp, device({ id: 'm2', space: 'other', primary: 'light.other', entities: ['light.other'],
marker: { id: 'm2', binding: 'device:m2' } })],
hass: { states: { 'light.led': { state: 'on', attributes: { rgb_color: [128, 213, 255] } } } },
defaultColor: '#ffd27b', paletteAlpha: 0.7, cellCm: 5, gridPitch: 1000 / 240, iconPct: 3.4,
scene: null, polygons: [], glowFor: () => true, inRoom: () => true, showHidden: false,
});
assert.deepEqual(frame.views.map(view => [view.strip.id, view.device.id]), [['valid', 'm1']],
'only the same-space bound strip may reach stripe, light and hit rendering');
assert.equal(frame.views[0].state, 'on');
assert.equal(frame.views[0].glow, true);
assert.equal(frame.views[0].appearance?.c, '#80d5ff', 'the valid neighbour retains its live light');
assert.deepEqual(strips, before, 'runtime filtering must not rewrite stored strips');
});
test('AC2/r1 M2: an explicit valid room_id wins over the anchor room; a stale one falls back', () => {
const rooms = [{ id: 'A' }, { id: 'B' }];
const inA = (point, room) => room.id === 'A' && point[0] < 500;