mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
v1.45.0: external review of v1.44.8 — R2-1, R2-2, R2-3
R2-1 (high): plan replacement committed filesystem state before the config CAS.
The upload wrote the final name and unlinked the other extension, so a rejected
config write left the live plan already replaced — or the stored config
pointing at a deleted file. Uploads now go to <space>.<token>.<ext> and delete
nothing; houseplan/plan/cleanup runs only after the config write is accepted.
The '.' separator is load-bearing: a space id cannot contain one, so cleaning
'f1' can never reach the files of 'f1-attic'.
R2-2: the backend signs at most MAX_SIGN_PATHS (200) per request and ignores
the rest silently, while the card sent its whole cache in one call and trusted
any cached entry forever — past 200 attachments the later ones stopped being
refreshed and expired for good. Requests are chunked to the shared constant,
entries carry their issue time (aging urls keep rendering while a replacement
is fetched, expired ones are dropped), and the cache is pruned to urls the live
config still references.
R2-3: areaClimate() rescanned the whole registry per room and per measurement.
areaClimateMap() classifies once and returns Map<area,{temp,hum}>, memoized on
hass identity so fresh states are always observed. Smoke measurement: 133
registry scans per update with 44 rooms before, 2 after, flat in room count.
Also: smoke_ux_fixes wrote its screenshot to a hard-coded /tmp path and could
not run on Windows.
Tests: smoke_plan_upload_reject, smoke_sign_cap, smoke_climate_once (all fail
on v1.44.8), three backend tests for versioned plan names and cleanup scoping,
unit tests for chunk/referencedContentUrls and areaClimateMap.
Docs: CHANGELOG.md + CHANGELOG.ru.md + ARCHITECTURE.md + TESTING.md + STATUS.md.
This commit is contained in:
+48
-1
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildDevices, lightGroups, primaryEntity, lqiFor, tempFor, humFor, areaLights, areaTemp, areaHum, areaLightStats, sourceValue , areaClimate } from '../test-build/devices.js';
|
||||
import { buildDevices, lightGroups, primaryEntity, lqiFor, tempFor, humFor, areaLights, areaTemp, areaHum, areaLightStats, sourceValue , areaClimate, areaClimateMap } from '../test-build/devices.js';
|
||||
import { compileIconRules, iconFor } from '../test-build/rules.js';
|
||||
|
||||
/** Minimal fake hass around the pieces buildDevices reads. */
|
||||
@@ -436,3 +436,50 @@ test('areaClimate: only ROOM AIR counts (field question, 2026-07-27)', () => {
|
||||
// остаётся ровно один настоящий датчик воздуха
|
||||
assert.equal(areaClimate(hass, 'bed', 'temp'), 22);
|
||||
});
|
||||
|
||||
test('areaClimateMap: one registry pass for all areas (review R2-3)', () => {
|
||||
// 60 зон × 2000 сущностей — размер боевой установки из отчёта
|
||||
const devices = {}; const entities = {}; const states = {};
|
||||
for (let a = 0; a < 60; a++) {
|
||||
for (let i = 0; i < 33; i++) {
|
||||
const dev = `d${a}_${i}`;
|
||||
const eid = `sensor.d${a}_${i}_temperature`;
|
||||
devices[dev] = { id: dev, name: `Датчик температуры ${a}.${i}`, area_id: `area${a}` };
|
||||
entities[eid] = { device_id: dev, platform: 'mqtt' };
|
||||
states[eid] = { state: String(20 + a * 0.1), attributes: { device_class: 'temperature', unit_of_measurement: '°C' } };
|
||||
}
|
||||
}
|
||||
// считаем ОБХОДЫ реестра: Object.entries дёргает ownKeys ровно один раз
|
||||
let scans = 0;
|
||||
const traced = new Proxy(entities, { ownKeys(t) { scans++; return Reflect.ownKeys(t); } });
|
||||
const hass = { devices, states, entities: traced };
|
||||
|
||||
|
||||
const map = areaClimateMap(hass);
|
||||
assert.equal(scans, 1, 'реестр обходится один раз на снимок hass');
|
||||
assert.equal(map.size, 60);
|
||||
assert.equal(map.get('area0').temp, 20);
|
||||
assert.equal(map.get('area59').temp, 25.9);
|
||||
assert.equal(map.get('area0').hum, null);
|
||||
assert.equal(map.get('nope'), undefined);
|
||||
|
||||
// старый путь: отдельный обход на каждую комнату и каждую величину
|
||||
scans = 0;
|
||||
for (let a = 0; a < 60; a++) { areaClimate(hass, `area${a}`, 'temp'); areaClimate(hass, `area${a}`, 'hum'); }
|
||||
assert.equal(scans, 120, 'wrapper считает по одной зоне — им нельзя пользоваться в рендере');
|
||||
});
|
||||
|
||||
test('areaClimateMap: температура и влажность живут в одной записи', () => {
|
||||
const hass = {
|
||||
devices: { q: { id: 'q', name: 'Qingping Air Monitor', area_id: 'hall' } },
|
||||
entities: {
|
||||
'sensor.q_t': { device_id: 'q', platform: 'xiaomi' },
|
||||
'sensor.q_h': { device_id: 'q', platform: 'xiaomi' },
|
||||
},
|
||||
states: {
|
||||
'sensor.q_t': { state: '21.4', attributes: { device_class: 'temperature', unit_of_measurement: '°C' } },
|
||||
'sensor.q_h': { state: '48', attributes: { device_class: 'humidity', unit_of_measurement: '%' } },
|
||||
},
|
||||
};
|
||||
assert.deepEqual(areaClimateMap(hass).get('hall'), { temp: 21.4, hum: 48 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user