Release v1.61.0-beta.8 candidate

This commit is contained in:
Matysh
2026-08-11 05:40:32 +03:00
parent 5c5833d69a
commit 48eebfd8a5
70 changed files with 2416 additions and 1588 deletions
+59
View File
@@ -0,0 +1,59 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
createRenderDeviceSnapshot, presentationSnapshotKey,
} from '../test-build/render-device-snapshot.js';
test('RenderDeviceSnapshot keeps immutable facts and excludes live HA capabilities', () => {
const state = { entity_id: 'light.one', state: 'on', attributes: { brightness: 120 } };
const device = { id: 'one', space: 'floor', entities: ['light.one'], icon: 'mdi:lightbulb' };
const presentation = { icon: 'mdi:lightbulb', valueText: null, visual: { status: 'working' } };
const snapshot = createRenderDeviceSnapshot({
sourceSequence: 7,
hass: {
states: { 'light.one': state }, entities: {}, devices: {},
connection: { live: true }, callService: () => undefined,
},
devices: [device],
positions: new Map([['one', { x: 12, y: 34 }]]),
presentations: new Map([[presentationSnapshotKey('one', true), presentation]]),
facts: new Map([['vacuum:one', { moving: true }]]),
});
state.attributes.brightness = 1;
device.entities.push('switch.other');
presentation.visual.status = 'neutral';
assert.equal(snapshot.sourceSequence, 7);
assert.equal(snapshot.hass.connection, undefined);
assert.equal(snapshot.hass.callService, undefined);
assert.equal(snapshot.hass.states['light.one'].attributes.brightness, 120);
assert.deepEqual(snapshot.devices[0].entities, ['light.one']);
assert.deepEqual(snapshot.positions.get('one'), { x: 12, y: 34 });
assert.equal(snapshot.presentations.get(presentationSnapshotKey('one', true)).visual.status, 'working');
assert.equal(snapshot.facts.get('vacuum:one').moving, true);
assert.equal('set' in snapshot.positions, false);
assert.equal('set' in snapshot.presentations, false);
assert.equal('set' in snapshot.facts, false);
});
const methodBody = (source, name) => {
const start = source.indexOf(`private ${name}(`);
assert.notEqual(start, -1, `${name} exists`);
const tail = source.slice(start + 1);
const next = tail.search(/\n (?:private|protected|public)\s/);
return source.slice(start, next < 0 ? source.length : start + 1 + next);
};
test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
for (const name of [
'_roomLqi', '_resolvedRoomFills', '_sunNow', '_renderSunRays', '_renderGlowLayer',
'_renderVacuums', '_renderVacFit', '_renderDevice', '_roomTemp', '_roomHum', '_openingAmt',
'_renderOpeningLocks', '_renderDecorLayer',
]) {
assert.doesNotMatch(methodBody(source, name), /this\.hass\b/, name);
}
});
+24 -5
View File
@@ -155,22 +155,24 @@ test('a stale recovery token can never commit', async () => {
assert.equal(controller.frameFingerprint, 'old');
});
test('a readiness loss rejects recoverably without masquerading as a timeout', async () => {
test('asset readiness is polled inside the bounded barrier and keeps a stale frame on timeout', async () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
controller.markCompleteFrame('old');
const token = controller.beginCandidate('asset-refresh', 'asset');
controller.candidateReady(token);
const committed = await controller.commitAfterPaint(token, {
const committed = controller.commitAfterPaint(token, {
updateComplete: async () => undefined,
stageValid: () => true,
assetsReady: () => false,
frameFingerprint: () => 'bad',
});
assert.equal(committed, false);
assert.equal(controller.state, 'holding');
await Promise.resolve();
clock.advance(PAINT_BARRIER_MAX_MS);
assert.equal(await committed, false);
assert.equal(controller.state, 'steady');
assert.equal(controller.frameFingerprint, 'old');
assert.equal(controller.trace.at(-1)?.event, 'paint-barrier-rejected');
assert.equal(controller.trace.at(-1)?.event, 'paint-barrier-timeout');
});
test('a real paint timeout without a complete frame becomes a recoverable error', async () => {
@@ -195,4 +197,21 @@ test('content fingerprints are stable across object key order and frame parts',
assert.equal(contentFingerprint({ b: 2, a: 1 }), contentFingerprint({ a: 1, b: 2 }));
assert.notEqual(contentFingerprint({ a: 1 }), contentFingerprint({ a: 2 }));
assert.equal(visualFrameFingerprint([1, 'x']), visualFrameFingerprint([1, 'x']));
const shared = { value: 1 };
assert.equal(
contentFingerprint({ a: shared, b: shared }),
contentFingerprint({ a: { value: 1 }, b: { value: 1 } }),
'a shared acyclic reference is hashed by value, not misclassified as a cycle',
);
});
test('trace diagnostics are returned as a defensive copy', () => {
const clock = new FakeClock();
const controller = new VisualContinuityController(() => undefined, clock);
controller.note('sample', { stage: [10, 20] });
const trace = controller.trace;
trace[0].event = 'mutated';
trace[0].stage[0] = 999;
assert.equal(controller.trace[0].event, 'sample');
assert.deepEqual(controller.trace[0].stage, [10, 20]);
});