mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
fix(zigbee): restore observers after integration reload (#800)
Issue: #800 User-Visible: yes
This commit is contained in:
@@ -424,6 +424,11 @@ class ZigbeeScanCoordinator:
|
||||
if self.closed:
|
||||
return
|
||||
self.closed = True
|
||||
# Reload does not disconnect HA WebSockets. Tell their observers that
|
||||
# this session and its volatile cache are gone before dropping them.
|
||||
self.revision += 1
|
||||
self._emit({"kind": "closed", "session_id": self.session_id,
|
||||
"revision": self.revision})
|
||||
tasks = [job.task for job in self._jobs.values() if job.task is not None]
|
||||
for job in self._jobs.values():
|
||||
self._release(job)
|
||||
|
||||
@@ -84,6 +84,11 @@ const result = await page.evaluate(async () => {
|
||||
await wait(() => !!settings?.shadowRoot?.querySelector('button'), 'settings button');
|
||||
await settings._readZha();
|
||||
await wait(() => settings._snapshot?.states?.zha?.phase === 'ready', 'ZHA ready');
|
||||
// #800: this fixture intentionally has the old integration's config/get.
|
||||
// Let its independent capability reply settle before injecting the age probe;
|
||||
// otherwise that real runtime notification replaces the synthetic timestamp.
|
||||
await wait(() => settings._snapshot?.backendError === 'backend_required', 'old backend detected');
|
||||
out.oldBackendStillAllowsZha = settings._snapshot.states.zha.phase === 'ready';
|
||||
out.explicitZhaRead = zhaCalls === 1;
|
||||
const settingsSnapshot = settings._snapshot;
|
||||
const renderedProviderStatus = () => [...settings.shadowRoot.querySelectorAll('.status,.hpf-actions .hpf-hint')]
|
||||
|
||||
@@ -10,10 +10,10 @@ const output = {};
|
||||
const record = (name, actual) => { output[name] = actual; check(name, actual); };
|
||||
const artifacts = new URL('../artifacts/zigbee-topology-800/', import.meta.url);
|
||||
mkdirSync(artifacts, { recursive: true });
|
||||
const server = { revision: 0, sequence: 0, publishes: 0, cancels: 0, provider: null };
|
||||
const state = () => ({ kind: 'state', session_id: 'server-800', revision: server.revision,
|
||||
const server = { session: 'server-800', revision: 0, sequence: 0, publishes: 0, cancels: 0, provider: null };
|
||||
const state = () => ({ kind: 'state', session_id: server.session, revision: server.revision,
|
||||
provider: structuredClone(server.provider) });
|
||||
const initial = () => [{ kind: 'reset', session_id: 'server-800', revision: server.revision,
|
||||
const initial = () => [{ kind: 'reset', session_id: server.session, revision: server.revision,
|
||||
topics: server.provider ? [server.provider.topic] : [] }, ...(server.provider ? [state()] : [])];
|
||||
await page.exposeFunction('__scanRpc', async (message) => {
|
||||
if (message.type === 'houseplan/zigbee/subscribe') return initial();
|
||||
@@ -185,6 +185,21 @@ try {
|
||||
document.getElementById('scan-settings')._snapshot.topologies.length === 1));
|
||||
await page.setViewportSize({ width: 1000, height: 850 });
|
||||
await page.screenshot({ path: fileURLToPath(new URL('desktop-ready.png', artifacts)) });
|
||||
const obsolete = state();
|
||||
await page.evaluate((event) => {
|
||||
for (const callback of window.__scanCallbacks) callback(event);
|
||||
}, { kind: 'closed', session_id: server.session, revision: ++server.revision });
|
||||
server.session = 'server-800-reloaded'; server.revision = 0; server.provider = null;
|
||||
await page.waitForFunction(() => !document.getElementById('scan-settings')._snapshot.states['z2m:zigbee2mqtt']);
|
||||
record('integrationUnloadClearsUiWithoutClosingHaSocket', await refresh().isEnabled()
|
||||
&& await page.evaluate(() => window.__scanStats.active === 0));
|
||||
await refresh().click(); await waitState('loading');
|
||||
await page.evaluate((event) => {
|
||||
for (const callback of window.__scanCallbacks) callback(event);
|
||||
}, obsolete);
|
||||
record('explicitScanAfterIntegrationReloadReobservesNewSession', server.publishes === 3
|
||||
&& await page.evaluate(() => window.__scanStats.active === 1
|
||||
&& document.getElementById('scan-settings')._snapshot.states['z2m:zigbee2mqtt'].jobId === 'job-3'));
|
||||
await page.evaluate(async () => {
|
||||
const element = document.getElementById('scan-settings');
|
||||
element.hass = { ...element.hass, user: { id: 'viewer-800', is_admin: false } };
|
||||
|
||||
@@ -616,6 +616,8 @@ cancel becomes available; it cancels HP's wait, not Z2M radio work. Matching
|
||||
provider errors, invalid maps and MQTT disconnect terminate the job, release
|
||||
listeners and retain last-good data as stale. Neither reconnect nor reopening
|
||||
settings republishes. Integration unload/restart clears all runtime jobs/cache.
|
||||
Before unload, a `closed` session event invalidates observers even when their HA
|
||||
WebSocket stays connected; the next explicit scan reattaches to the new coordinator.
|
||||
The frontend derives elapsed time from server elapsed plus local monotonic time,
|
||||
without per-second map events or invented progress percentages. Mounted visible
|
||||
settings alone tick their timer; ordinary hass updates do not resubscribe.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
after 10 minutes you can cancel House Plan's wait without stopping Zigbee2MQTT
|
||||
or losing the last successful map. Update and restart the integration too
|
||||
([#800](https://github.com/Matysh/houseplan-card/issues/800)).
|
||||
- Reloading the integration also clears an old scan from an already-open page,
|
||||
so its next explicit scan can attach to the new session ([#800](https://github.com/Matysh/houseplan-card/issues/800)).
|
||||
|
||||
## v1.80.0-beta.2 — 2026-10-05
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
после 10 минут можно отменить ожидание House Plan, не останавливая Zigbee2MQTT
|
||||
и не теряя последнюю полученную карту. Обновите и перезапустите также интеграцию
|
||||
([#800](https://github.com/Matysh/houseplan-card/issues/800)).
|
||||
- После перезагрузки интеграции старое ожидание сбрасывается и на уже открытой
|
||||
странице: следующий явный запуск подключается к новой сессии ([#800](https://github.com/Matysh/houseplan-card/issues/800)).
|
||||
|
||||
## v1.80.0-beta.2 — 2026-10-05
|
||||
|
||||
|
||||
+4
-4
@@ -19,18 +19,18 @@ Everything computable from the tree and git; regenerate, never edit by hand
|
||||
| Generated | 2026-10-05 — rerun `node scripts/status-snapshot.mjs` for the current tree |
|
||||
| Version | **1.80.0-beta.2** in all 7 version sources (`scripts/release-contract.mjs`) |
|
||||
| Latest stable tag | `v1.79.0` |
|
||||
| Latest prerelease tag | `v1.80.0-beta.1` |
|
||||
| Tests | Node unit 3607 · pure backend 423 · HA-harness backend 311 · browser smokes 295 (`npm run inventory`) |
|
||||
| Latest prerelease tag | `v1.80.0-beta.2` |
|
||||
| Tests | Node unit 3610 · pure backend 423 · HA-harness backend 330 · browser smokes 296 (`npm run inventory`) |
|
||||
<!-- status-snapshot:end -->
|
||||
|
||||
## Current cycle and standing decisions
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| Current local cycle | **Beta v1.80.0-beta.2 candidate** — #799 fixes premature timeout of long Zigbee2MQTT route scans. Owner-authorized express local checks; full exact-SHA Validate and artifact verification precede publication. `main` remains on stable v1.79.0. |
|
||||
| Current local cycle | **#800 implementation and independent local review** — owner approved background Zigbee2MQTT scans and the next beta after completion. Full exact-SHA Validate and artifact verification precede publication. `main` remains on stable v1.79.0. |
|
||||
| Branches | `main` carries stable releases only; pre-release tags point at `dev`. Work lands on `dev`, which is equal to or ahead of `main`, never behind. |
|
||||
| Zigbee routes | #798 replaces inferred neighbour trees with integration-reported end-parent and active coordinator next-hop evidence. Unknown/conflicting routes are not guessed; stale/partial snapshots remain labelled. Solid arrows have a separate 0–255 palette; ordinary device LQI colours are unchanged. |
|
||||
| Zigbee scan deadline | #799 gives route-table scans one 10-minute total budget, with separate 10-second MQTT setup/publish caps and the existing 4-second retained-info check. Fake-clock regressions cover late success, hard expiry, stale-cache retention and cleanup. |
|
||||
| Zigbee scan lifecycle | #800 replaces #799's 10-minute total budget with integration-owned background jobs: closing the browser does not stop waiting; after 10 minutes an exact-job cancel becomes available. MQTT setup/publish caps and the retained-info check remain bounded. Tests cover 15-minute success, cross-client restore, cancel races, stale-cache retention and reload on a live HA connection. |
|
||||
| LED regression acceptance | #788 covers mixed face/free and fractional-coordinate joins, winding-safe field unions, retained endpoints/acute vertices and circle–wall transition events. The actual owner export is exercised locally; public fixtures are synthetic. Pixel oracles, not just SVG counts, protect the visible falloff. |
|
||||
| LED core colour | #790 makes the on core follow the resolved light colour with or without Glow. Off, unavailable and non-live static strips keep their neutral presentation; geometry and field brightness are unchanged. |
|
||||
| LED performance work | #789 batches source entry, reuses render-local barriers and temporarily paints 24 of 48 retained gradient bands during actual zoom; full quality returns after 160 ms of scale inactivity. Emitters, clips and idle pixels are unchanged. Exact-SHA 50×50 acceptance includes restoration and quick restart; implementation alone does not certify the camera budget. |
|
||||
|
||||
+10
-2
@@ -344,8 +344,16 @@ For an occasional Zigbee placement check, an administrator can enable
|
||||
is off by default. Load the provider snapshot there: **Read ZHA data** reads
|
||||
ZHA's existing cache, while **Update map** starts an explicit Zigbee2MQTT raw
|
||||
network-map scan for each entered base topic (default `zigbee2mqtt`). The latter
|
||||
may take several minutes when collecting routes. House Plan waits up to 10 minutes;
|
||||
do not start another scan while it is loading. Scanning can temporarily slow the Zigbee network.
|
||||
may take many minutes when collecting routes. The scan continues in Home Assistant
|
||||
after you close settings or the browser. Reopening settings restores its stage and
|
||||
elapsed timer, including on another administrator's device. Progress is not a
|
||||
percentage: Zigbee2MQTT does not provide one. After 10 minutes, waiting continues
|
||||
and **Cancel request** becomes available. It only stops House Plan waiting for this
|
||||
result, not the radio scan in Zigbee2MQTT. Do not start another scan until that scan
|
||||
has finished; scanning can temporarily slow the Zigbee network. The last successful
|
||||
map stays available, marked stale after an error or cancellation. Restarting Home
|
||||
Assistant or reloading the integration clears these temporary jobs and cached maps.
|
||||
Update and restart the House Plan integration as well as updating the card.
|
||||
|
||||
After data is loaded, moving a real mouse over a mapped Zigbee marker shows
|
||||
its parent (end device) or active next hop towards the coordinator (router),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Локальное предварительное ревью ТЗ #800
|
||||
# Локальная предварительная проверка ТЗ #800 (до канонического r1)
|
||||
|
||||
Итог второго предварительного прохода: зелёный.
|
||||
High: 0 · Medium: 0 · Low: 0.
|
||||
@@ -234,6 +234,36 @@
|
||||
"since": "1.60.2-beta.1",
|
||||
"audience": ["styling", "test"]
|
||||
},
|
||||
"zigbee-scan-background": {
|
||||
"elements": ["span"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zigbee-scan-cancel": {
|
||||
"elements": ["button"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zigbee-scan-cancel-hint": {
|
||||
"elements": ["span"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zigbee-scan-elapsed": {
|
||||
"elements": ["span"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zigbee-scan-long-wait": {
|
||||
"elements": ["span"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zigbee-scan-stage": {
|
||||
"elements": ["span"],
|
||||
"since": "1.80.0-beta.3",
|
||||
"audience": ["test"]
|
||||
},
|
||||
"zoom-fit": {
|
||||
"elements": ["button"],
|
||||
"since": "1.73.0-beta.7",
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Независимое локальное код-ревью #800 — r1
|
||||
|
||||
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 3 → в задаче · Low: 1 · маршрут: fix.
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Дата: 2026-10-05. Ревьюер: отдельный агент `/root/code_review_800`, не автор
|
||||
ТЗ, не ревьюер ТЗ и не исполнитель реализации.
|
||||
- Issue: [#800](https://github.com/Matysh/houseplan-card/issues/800), `track:ask`.
|
||||
- Ветка: `issue/800-zigbee-background-scan`.
|
||||
- База: `1faed2af7d89f768e7fd15a55489c5f8317f7032`.
|
||||
- Продуктовый материал: `379de706d29f4b1547c3b162c1c205654487c6b8`.
|
||||
26 файлов, 2294 добавленных и 784 удалённых строки. SHA повторно проверен
|
||||
перед составлением вердикта. Трейлеры `Issue: #800`, `User-Visible: yes`
|
||||
и оба changelog находятся в продуктовом коммите.
|
||||
- ТЗ: полное тело issue, сверенное с переданным `hp800-spec.md`; SHA-256
|
||||
нормализованного тела из принятого ТЗ:
|
||||
`de6f7a8816ce796cb3f53713838505a66a5b098d986cf39d56f9e8ec74a55d09`.
|
||||
|
||||
Локальный независимый порядок разрешён владельцем
|
||||
[только для #800](https://github.com/Matysh/houseplan-card/issues/800#issuecomment-5998860326).
|
||||
Это не результат штатной модели и не подтверждение CI/слияния/выпуска.
|
||||
Ревьюер не менял продукт, метки, GitHub, коммиты или ветки.
|
||||
|
||||
Во время разбора автор отдельно поправил только гонку фикстуры в
|
||||
`demo/smoke_zigbee_topology_hover.mjs`: ожидание `backend_required` перед
|
||||
искусственной подстановкой возраста ZHA. Прочитан незакоммиченный diff;
|
||||
SHA-256 файла `332f80111ee4c3f06e2a18643cbee11eaaf08460102aa2e6444daf9364d13d96`.
|
||||
Он не подменяет материал `379de706d`. Generated `dist/` после гейта также
|
||||
не входит в рассматриваемый продуктовый коммит. Последующие исправления
|
||||
находок этого отчёта требуют r2 по дельте.
|
||||
|
||||
## Скоуп и способ проверки
|
||||
|
||||
Работа обслуживает J7: пространственную диагностику Zigbee. Прочитаны новый
|
||||
coordinator/WS, lifecycle интеграции, frontend runtime/settings/overlay,
|
||||
тесты, локали, изменения документации и декларации мутантов. Алгоритмы
|
||||
маршрутов/LQI и сохранённая геометрия не меняются.
|
||||
|
||||
| Проверка | Результат и происхождение |
|
||||
| --- | --- |
|
||||
| `git diff --check 1faed2af..379de706d` | PASS, исполнено ревьюером |
|
||||
| `node --test test/zigbee-topology.test.mjs test/zigbee-topology-runtime-routes.test.mjs test/zigbee-provider-routes.test.mjs test/zigbee-topology-style.test.mjs` | 52/52 PASS, исполнено ревьюером на подготовленном `test-build`; полный typecheck/build — ниже |
|
||||
| `node scripts/mutation-gate.mjs --check` | PASS, исполнено ревьюером; предупреждения существующего browser inventory. Мутанты не исполнялись по PROCESS §2.7 |
|
||||
| `npm run gate:small` | FAIL, полный лог автора `C:/Temp/hp800-gate-small.log` прочитан ревьюером: 3612 PASS, 6 FAIL, 62 SKIP; отдельно FAIL `lint:unused` по bundleBytes. Детали M3 |
|
||||
| `npm run typecheck`, `npm run bundle:sync` | PASS по переданному автором результату; build/typecheck также прошёл до unit-стадии полного гейта |
|
||||
| `/home/matysh/houseplan-card-576-final/.venv-ci/bin/python -m pytest tests_backend/test_ha_zigbee_topology.py -q --tb=short` | 32 PASS по результату backend-исполнителя; HA 2026.8.3 / pytest-homeassistant-custom-component 0.13.357, WSL Ubuntu |
|
||||
| Тот же Python: `-m pytest tests_backend -q --tb=short` | 1007 PASS, 1 SKIP по результату backend-исполнителя; не нативный Windows pure-only прогон |
|
||||
| `/home/matysh/houseplan-card-576-final/.venv-ci/bin/ruff check custom_components/houseplan` | PASS по результату backend-исполнителя |
|
||||
| Node 22.23.2: `node demo/smoke_zigbee_topology_job.mjs` | 15/15 PASS по результату автора в `/home/matysh/hp800-6yPzWT`; ревьюер прочитал assertions и просмотрел narrow light/dark PNG |
|
||||
| `node demo/smoke_general_settings_form.mjs` | 30/30 PASS по результату автора |
|
||||
| `node demo/smoke_zigbee_topology_hover.mjs` | Первоначально FAIL на искусственном возрасте; после указанного выше fixture-only изменения PASS по результату автора. Не считать исходный smoke зелёным на неизменённом материале |
|
||||
| Reload-проба текущего production runtime | FAIL ожидаемого восстановления, исполнено ревьюером без изменения файлов; M1 |
|
||||
|
||||
WSL-проверки исполнителя запускались через `wsl -d Ubuntu --cd
|
||||
/home/matysh/hp800-6yPzWT -- ...`. Эта копия содержала актуальные файлы, но её
|
||||
Git HEAD оставался базовым: это локальное доказательство исполнения файлов,
|
||||
не exact-SHA CI attestation. Backend-логи ревьюером повторно не исполнялись;
|
||||
числа переданы исполнителем и не выдаются за собственный прогон.
|
||||
|
||||
### smoke-select
|
||||
|
||||
Исполнена команда `node scripts/smoke-select.mjs --base
|
||||
1faed2af7d89f768e7fd15a55489c5f8317f7032 --head
|
||||
379de706d29f4b1547c3b162c1c205654487c6b8`: 12 прямых и 2
|
||||
зарегистрированных совпадения. Решения перечислены полностью:
|
||||
|
||||
| Строка выборки | Решение |
|
||||
| --- | --- |
|
||||
| `smoke_zigbee_topology_hover`, `smoke_zigbee_topology_job` | Применимы; результаты выше |
|
||||
| `smoke_bg_color` | Не исполнен: совпало общее имя `_snapshot`, фон не менялся |
|
||||
| `smoke_danger_confirm_branches`, `smoke_dialog_help_clipping`, `smoke_dialog_modal_recovery`, `smoke_ha_form_shell_parity`, `smoke_preloader_lifecycle`, `smoke_space_card_bg`, `smoke_summary_dialog_scroll` | Не исполнены: совпало общее имя `connectedCallback`; соответствующие компоненты не менялись. Общие настройки дополнительно покрыты профильным smoke |
|
||||
| `smoke_support_feedback` | Не исполнен: `errorCode` здесь локальная функция другого модуля, support не менялся |
|
||||
| `smoke_warm_owners` | Не исполнен: совпало общее имя `disconnectedCallback`; lifecycle самого Zigbee проверяется профильным smoke/runtime suite |
|
||||
| `smoke_resize_pointer_real_plan`, `smoke_resize_wall_thickness` | Не исполнены: связь по удалённому локальному `keyOf` Zigbee совпала с независимым геометрическим символом; resize не менялся |
|
||||
|
||||
Полная smoke-матрица остаётся предрелизной проверкой. Этот отчёт не объявляет
|
||||
неисполненные строки зелёными.
|
||||
|
||||
## Находки
|
||||
|
||||
### M1 — живой браузер остаётся на умершем job после reload интеграции
|
||||
|
||||
`custom_components/houseplan/zigbee_topology.py:422–436` очищает `_listeners`
|
||||
без сообщения существующим WS-наблюдателям. При reload только интеграции
|
||||
соединение HA WebSocket остаётся открытым. Frontend сохраняет `unsubscribe`
|
||||
старого coordinator и старый `session`; `startFeed` не создаёт новую подписку,
|
||||
пока старый unsubscribe существует, а `accept` отбрасывает ответ команды
|
||||
нового coordinator как чужую сессию (`src/zigbee-topology-runtime.ts:102,
|
||||
142, 169–172`).
|
||||
|
||||
Воспроизведение: оставить настройки/overlay подписанными, запустить scan,
|
||||
reload интеграции без перезагрузки браузера. Старое ожидание продолжает
|
||||
рисоваться, «Обновить карту» остаётся disabled. После 600 секунд отмена
|
||||
старого id получает `conflict`, который frontend намеренно игнорирует.
|
||||
Даже явный start нового job из того же runtime не принимается клиентским
|
||||
кешем. Закрытие одних настроек не помогает, пока overlay удерживает общий feed.
|
||||
|
||||
Ревьюер исполнил непереписывающую Node-пробу: production TypeScript
|
||||
транспилирован в памяти, принят `old-job`, затем смоделирована фактическая
|
||||
очистка listener при смене coordinator, вызваны cancel и start новой сессии.
|
||||
Вывод: после reload `phase: loading`, `jobId: old-job`, `serverStarts: 2`,
|
||||
`observers: 0`. Контракт §4.7/§6 и AC4/AC6 нарушен.
|
||||
|
||||
Нужно передать клиенту инвалидирование старой серверной сессии и обеспечить
|
||||
восстановление наблюдения после reload, без автоматического сканирования.
|
||||
Нужен тест с подписанным клиентом до reload и тем же живым WS после него.
|
||||
Нынешний backend unload-тест до выгрузки вообще не подписывается.
|
||||
|
||||
### M2 — AC1 не имеет свидетеля, чувствительного к прежнему 600-секундному deadline
|
||||
|
||||
`tests_backend/test_ha_zigbee_topology.py:142–147, 175–219` изменяет только
|
||||
импортированные `zigbee.monotonic` и `zigbee.time`. `clock.now += 600/300`
|
||||
не продвигает `hass.loop.time()` и очередь `asyncio`-таймеров. Следовательно,
|
||||
добавление прежнего общего `asyncio.timeout(600)` вокруг ожидания карты не
|
||||
успеет сработать в этом тесте: он всё равно получит немедленный fake ответ
|
||||
и будет зелёным. Frontend smoke также продвигает число в fake server state,
|
||||
а не backend deadline.
|
||||
|
||||
Чтением подтверждено, что нынешний `_run` общего таймаута не содержит; это
|
||||
не доказательство требуемой регрессии. AC1 прямо требует отрицательную пробу
|
||||
прежнего лимита, а PROCESS §2.7 — названный свидетель защиты. Соответствующей
|
||||
декларации в `scripts/mutation-registry.mjs` нет.
|
||||
|
||||
Нужно управлять реальным временем event loop/запланированными timeout callbacks
|
||||
в witness и зарегистрировать мутацию восстановления общего deadline. Мутант
|
||||
в этом цикле не запускать: его исполнение остаётся ночи.
|
||||
|
||||
### M3 — материал не проходит обязательный gate:small
|
||||
|
||||
В прочитанном полном логе есть относящиеся к новому материалу отказы:
|
||||
|
||||
- `test/data-hp-contract.test.mjs`: шесть новых `zigbee-scan-*` хуков не
|
||||
объявлены публичными или внутренними.
|
||||
- `test/reviews-index.test.mjs`: `PREFLIGHT-SPEC-800.md` не соответствует
|
||||
схеме имён; индекс новых review-документов не приведён в согласованное состояние.
|
||||
- `test/smoke-select.test.mjs`: новые backend `.py` пути в `SMOKE_LINKS`
|
||||
нарушают существующую схему `src/*.ts`.
|
||||
- `lint:unused`: `bundleBytes` 2 696 965 против базы 2 686 188, рост 10 777 Б
|
||||
при полосе 2 000 Б. Не приложено требуемое обоснование/согласованная база.
|
||||
|
||||
Это воспроизводимый отказ обязательного AC-гейта, не допустимое умолчание
|
||||
перед merge. Исправить декларации и размер/его процессное обоснование,
|
||||
повторить полный gate в каноническом окружении. Три остальных Windows-отказа
|
||||
(`dev-build`, `nightly-workflow`, `iso-overlay-fixture-types`) отдельно
|
||||
не объявляются продуктовыми регрессиями #800: требуется результат Linux/WSL,
|
||||
но нельзя заранее засчитать их зелёными.
|
||||
|
||||
### L1 — English guide всё ещё обещает прекращение ожидания через 10 минут
|
||||
|
||||
`docs/USER-GUIDE.md:347` по-прежнему говорит «House Plan waits up to 10 minutes».
|
||||
Это прямо противоположно новой функции. ТЗ §10 требует USER-GUIDE RU/EN и
|
||||
STATUS; English guide и STATUS не изменены. Исправить вместе с возвратом
|
||||
задачи; это не основание расширять продуктовый скоуп.
|
||||
|
||||
## AC и «чем краснеет»
|
||||
|
||||
| AC | Чем доказан / граница доказательства | Чем краснеет |
|
||||
| --- | --- | --- |
|
||||
| AC1 | Чтением отсутствие scan deadline; backend `test_issue_800_background_15_minutes_two_clients_and_reopen` доказывает elapsed/приём ответа без UI, но не прохождение реального deadline | Недостающий отрицательный witness прежнего лимита — M2 |
|
||||
| AC2 | Backend two-clients/reopen; frontend `one shared feed`, `late 15m result`; browser reload | Повтор start обязан оставить publications=1; detach обязан оставить server job живым; cleanup mutant `zigbee-topology-z2m-subscriptions-leak` |
|
||||
| AC3 | Backend cancel-boundary/old-id/race; frontend 599/600/hours, cancelled-last-good; keyboard smoke | 599 отказ / 600 разрешение, старый id при уже cancellable новом job; мутанты `zigbee-background-stale-cancel-affects-new-job`, `zigbee-background-cancel-too-early` |
|
||||
| AC4 | Runtime ownership/late-ack/identity tests; browser elapsed/one subscription/detach/admin loss; просмотр light/dark PNG | Late callback после detach, stale action response, identity replacement не должны менять новый snapshot. Reload живого WS не покрыт и сломан — M1 |
|
||||
| AC5 | HA permission/topic/retained/foreign/oversize/invalid-shape/instant-error/disconnect/timeout/capacity/teardown tests; frontend old-backend test | Реальные non-admin WS отказы; неверная transaction/retain/oversize сохраняют last-good; 9-й active slot отвергнут; registry `zigbee-topology-z2m-foreign-response-accepted`, `zigbee-topology-z2m-malformed-response-waits-for-timeout`; timeout/disconnect negative cases в тестах |
|
||||
| AC6 | 52 unit PASS включая прежние route fixtures, ZHA cached read; чтением отсутствие сериализации runtime; backend unload очищает runtime | ZHA asserts точный `zha/devices` и никогда radio update; мутант `zigbee-topology-zha-read-starts-scan`; frontend forbidden MQTT call; integration reload клиента требует M1 |
|
||||
|
||||
## Что корректно и что не проверялось
|
||||
|
||||
Корректны по чтению и названным тестам: резервирование job до async работы,
|
||||
один запрос на topic, transaction/job-id isolation, admin-only WS независимо
|
||||
от editor policy, bounded payload/slots, last-good/stale, очистка поздних
|
||||
subscribe acknowledgements, немедленная correlated provider error даже внутри
|
||||
publish, отсутствие frontend MQTT fallback и отсутствие автоматических retry.
|
||||
ZHA, resolver routes/LQI и persisted store version не переписаны.
|
||||
|
||||
Число, видимое на обеих UI-поверхностях, — elapsed: обе вызывают один
|
||||
`zigbeeScanElapsedMs` и `formatZigbeeScanElapsed`. Сервер авторитетен по
|
||||
600-секундному порогу, передаёт `cancel_after_ms`; frontend fallback не
|
||||
ослабляет серверный guard. Нет ежесекундной передачи карты.
|
||||
|
||||
На заявленном HA floor наличие `ConfigEntry.async_create_background_task`
|
||||
и MQTT connection-status API сверено чтением официального
|
||||
[HA 2024.6.4 config_entries](https://github.com/home-assistant/core/blob/2024.6.4/homeassistant/config_entries.py)
|
||||
и [MQTT module](https://github.com/home-assistant/core/blob/2024.6.4/homeassistant/components/mqtt/__init__.py).
|
||||
Отсутствие необязательного readiness helper отдельно моделируется тестом;
|
||||
полный запуск старого HA не выполнялся.
|
||||
|
||||
Риски: async/data/permissions/host lifecycle применимы и рассмотрены;
|
||||
геометрия не менялась, invariants не требуются. Визуал ограничен настройками;
|
||||
narrow light/dark изображения просмотрены, cancel не обрезан. Touch editor:
|
||||
not exposed; новые настройки доступны touch, целевой размер проверен smoke,
|
||||
настоящий touch-device/экранный диктор не испытывались. Работа диктора
|
||||
проверена только чтением `role=status` и `aria-live=off` у таймера.
|
||||
|
||||
Реальная сеть владельца из 67 устройств, Linux full Validate точного SHA,
|
||||
полная smoke/golden/performance матрица и выпуск не проверялись. Это не
|
||||
обещание скорости радио-сканирования Z2M. Golden не назначен ТЗ/меткой;
|
||||
runtime-кеш ограничен чтением/тестами, отдельный performance benchmark не
|
||||
назначен. До закрытия M1–M3 зелёный вердикт и merge невозможны.
|
||||
@@ -1,12 +1,14 @@
|
||||
# Индекс ревью
|
||||
|
||||
Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 301, issue: 151. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
|
||||
Генерируется `node scripts/reviews-index.mjs` (#635) — не редактировать руками. Документов: 303, issue: 152. Вердикт: 🟢 зелёный · 🟡 жёлтый · 🔴 красный · ⚪ не распознан (свободная форма старых документов). H/M — число High/Medium по строке вердикта или заголовкам находок. Файлы — пути, названные в находках; ищите по имени файла: `grep form-kit INDEX.md`.
|
||||
|
||||
| Issue | Документ | Этап · раунд | Вердикт | H | M | Находки | Файлы |
|
||||
|---|---|---|---|---:|---:|---|---|
|
||||
| бета v1.80.0-beta.1 | [SHIP-REVIEW-v1.80.0-beta.1.md](SHIP-REVIEW-v1.80.0-beta.1.md) | пакетное ревью ship · — | ⚪ — | 0 | 0 | — | — |
|
||||
| бета v1.79.0-beta.2 | [SHIP-REVIEW-v1.79.0-beta.2.md](SHIP-REVIEW-v1.79.0-beta.2.md) | пакетное ревью ship · — | ⚪ — | 0 | 0 | — | — |
|
||||
| бета v1.79.0-beta.1 | [SHIP-REVIEW-v1.79.0-beta.1.md](SHIP-REVIEW-v1.79.0-beta.1.md) | пакетное ревью ship · — | ⚪ — | 0 | 0 | — | — |
|
||||
| #800 | [SPEC-REVIEW-800-r1.md](SPEC-REVIEW-800-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | — | — |
|
||||
| #800 | [CODE-REVIEW-800-r1.md](CODE-REVIEW-800-r1.md) | code · r1 | 🟡 жёлтый | 0 | 3 | живой браузер остаётся на умершем job после reload интеграции; AC1 не имеет свидетеля, чувствительного к прежнему 600-секундному deadline; материал не проходит обязательный gate:small; English guide всё ещё обещает прекращение ожидания через 10 минут | `custom_components/houseplan/zigbee_topology.py` `scripts/mutation-registry.mjs` `test/data-hp-contract.test.mjs` `test/reviews-index.test.mjs` `PREFLIGHT-SPEC-800.md` `test/smoke-select.test.mjs` `docs/USER-GUIDE.md` |
|
||||
| #799 | [CODE-REVIEW-799-r1.md](CODE-REVIEW-799-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — |
|
||||
| #798 | [SPEC-REVIEW-798-r1.md](SPEC-REVIEW-798-r1.md) | spec · r1 | 🟢 зелёный | 0 | 0 | — | — |
|
||||
| #798 | [CODE-REVIEW-798-r1.md](CODE-REVIEW-798-r1.md) | code · r1 | 🟢 зелёный | 0 | 0 | — | — |
|
||||
|
||||
@@ -20,7 +20,7 @@ SHA базы материала: `1faed2af7d89f768e7fd15a55489c5f8317f7032`.
|
||||
не результат неотработавшей модели. Сбой штатного S4 не считается
|
||||
отрицательным содержательным вердиктом.
|
||||
|
||||
Предварительные проходы, описанные в [PREFLIGHT-SPEC-800.md](PREFLIGHT-SPEC-800.md),
|
||||
Предварительные проходы, описанные в [800-spec-preflight.md](../analysis/800-spec-preflight.md),
|
||||
не занимают канонические r1/r2 и не расходуют блокирующие циклы. Текущее тело
|
||||
побайтно после штатной нормализации совпадает с зелёным материалом второго
|
||||
preflight-прохода. Обе предварительные Medium исправлены до этого r1.
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"hostRefs": 5067,
|
||||
"portPrivates": 103,
|
||||
"harnessPrivates": 100,
|
||||
"bundleBytes": 2686188
|
||||
"bundleBytes": 2697519
|
||||
}
|
||||
|
||||
@@ -9866,6 +9866,30 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' if (monotonic() - job.started) * 1000 < 0:',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-background-scan-restores-ten-minute-deadline',
|
||||
guard: 'node scripts/backend-test-guard.mjs issue_800_background_15_minutes_two_clients_and_reopen '
|
||||
+ 'tests_backend/test_ha_zigbee_topology.py',
|
||||
because: '#800 AC1: advancing the actual asyncio loop clock through 600 and 900 seconds '
|
||||
+ 'must keep the server job waiting; the cancel threshold is not a scan deadline',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/zigbee_topology.py',
|
||||
find: ' await self.hass.loop.create_future()',
|
||||
replace: ' async with asyncio.timeout(600):\n await self.hass.loop.create_future()',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'zigbee-background-unload-leaves-ui-waiting',
|
||||
guard: 'node scripts/backend-test-guard.mjs issue_800_entry_unload_clears_jobs_and_new_entry_session_is_idle '
|
||||
+ 'tests_backend/test_ha_zigbee_topology.py',
|
||||
because: '#800 AC4/AC6: integration reload does not close HA WebSocket; existing observers '
|
||||
+ 'must receive invalidation before their coordinator is discarded',
|
||||
patches: [{
|
||||
file: 'custom_components/houseplan/zigbee_topology.py',
|
||||
find: ' self._emit({"kind": "closed", "session_id": self.session_id,\n "revision": self.revision})',
|
||||
replace: ' pass # mutant: silently discard subscribers',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'touch-pinch-click-block-cleared-on-terminal',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
/** @type {SmokeLink[]} */
|
||||
export const SMOKE_LINKS = [
|
||||
{
|
||||
files: ['src/zigbee-topology-runtime.ts', 'src/hp-zigbee-topology-settings.ts',
|
||||
'custom_components/houseplan/zigbee_topology.py', 'custom_components/houseplan/zigbee_websocket.py'],
|
||||
files: ['src/zigbee-topology-runtime.ts', 'src/hp-zigbee-topology-settings.ts'],
|
||||
smokes: ['smoke_zigbee_topology_job.mjs'],
|
||||
because: '#800: background-job restore across a real page reload, elapsed/cancel boundary, '
|
||||
+ 'mounted UI lifecycle and mobile keyboard target are observed through the production '
|
||||
|
||||
@@ -90,6 +90,18 @@ function accept(cache: Cache, message: unknown, fromSubscription: boolean): void
|
||||
if (!envelope || typeof envelope.session_id !== 'string' || !Number.isSafeInteger(envelope.revision)) return;
|
||||
const revision = envelope.revision as number;
|
||||
if (revision < 0) return;
|
||||
if (envelope.kind === 'closed') {
|
||||
if (!fromSubscription || envelope.session_id !== cache.session || revision < cache.serverRevision) return;
|
||||
// Integration reload does not close the HA socket. Retire this observer and
|
||||
// all pending old-session work, but retain mounted UI listeners for recovery.
|
||||
cache.generation++;
|
||||
cleanup(cache.unsubscribe); cache.unsubscribe = undefined;
|
||||
cache.feedPending = cache.capability = undefined;
|
||||
cache.session = undefined; cache.serverRevision = -1; cache.topicRevisions.clear();
|
||||
for (const key of cache.inflight.keys()) if (key.startsWith('z2m:')) cache.inflight.delete(key);
|
||||
removeTopics(cache, new Set());
|
||||
cache.backendConnected = false; cache.backendError = 'backend_required'; notify(cache); return;
|
||||
}
|
||||
if (envelope.kind === 'reset') {
|
||||
if (!fromSubscription || !Array.isArray(envelope.topics) || envelope.topics.length > 8) return;
|
||||
if (cache.session === envelope.session_id && revision < cache.serverRevision) return;
|
||||
@@ -216,6 +228,14 @@ function command(hass: ZigbeeTopologyHass, baseTopic: string, jobId?: string): P
|
||||
const promise = (async () => {
|
||||
requireAdmin(hass); if (!topic) throw fail('invalid_topic');
|
||||
await capability(cache, hass); if (!active()) return;
|
||||
if (jobId === undefined) {
|
||||
// Explicit start also recovers an observer invalidated by integration reload.
|
||||
// Joining it never publishes; only the command below starts a server job.
|
||||
startFeed(cache, hass);
|
||||
await cache.feedPending;
|
||||
if (!active()) return;
|
||||
if (cache.listeners.size && !cache.unsubscribe) throw fail(cache.backendError || 'backend_required');
|
||||
}
|
||||
const result = await hass.callWS!({ type: `houseplan/zigbee/${jobId === undefined ? 'start' : 'cancel'}`,
|
||||
base_topic: topic, ...(jobId === undefined ? {} : { job_id: jobId }) });
|
||||
if (active()) accept(cache, result, false);
|
||||
@@ -228,7 +248,7 @@ function command(hass: ZigbeeTopologyHass, baseTopic: string, jobId?: string): P
|
||||
if (jobId === undefined && cache.states[key]?.phase !== 'loading') {
|
||||
state(cache, key, { ...cache.states[key], phase: 'error', error: code, stale: cache.states[key]?.obtainedAt !== undefined });
|
||||
} else { cache.backendError = code; notify(cache); }
|
||||
}).finally(() => cache.inflight.delete(actionKey));
|
||||
}).finally(() => { if (cache.inflight.get(actionKey) === promise) cache.inflight.delete(actionKey); });
|
||||
cache.inflight.set(actionKey, promise); return promise;
|
||||
}
|
||||
/** Quick server-owned job start: no browser MQTT, scan timer, or fallback. */
|
||||
|
||||
@@ -281,3 +281,76 @@ test('#800 stale action response cannot roll back later streamed terminal state'
|
||||
provider: { ...provider, phase: 'loading', elapsed_ms: 0 } });
|
||||
await pending; assert.equal(b.snapshot().states[KEY].phase, 'ready'); assert.equal(b.snapshot().states[KEY].elapsedMs, 900000); off();
|
||||
});
|
||||
|
||||
test('#800 integration closed invalidates mounted jobs; explicit same-WS start reattaches without duplicate scan', async () => {
|
||||
const b = backend(), off = b.observe(); await refreshZ2mTopology(b.hass, TOPIC);
|
||||
b.update(TOPIC, { phase: 'ready', result: map, obtained_at: 80 });
|
||||
await refreshZ2mTopology(b.hass, TOPIC);
|
||||
const oldCallback = b.callbacks[0], oldState = b.envelope(b.jobs.get(TOPIC));
|
||||
const oldPublications = b.publications;
|
||||
b.emit({ kind: 'closed', session_id: b.session, revision: ++b.revision });
|
||||
assert.deepEqual(b.snapshot().states, {}, 'unloaded coordinator cannot leave disabled Update map/loading UI');
|
||||
assert.deepEqual(b.snapshot().topologies, [], 'unloaded coordinator last-good cache is gone');
|
||||
assert.equal(b.snapshot().backendConnected, false);
|
||||
assert.equal(b.cleanups, 1); assert.equal(b.live.size, 0);
|
||||
await turn(); assert.equal(b.publications, oldPublications, 'close never scans or automatically retries');
|
||||
const closedRevision = b.snapshot().revision;
|
||||
oldCallback({ ...oldState, revision: 999 });
|
||||
oldCallback({ kind: 'reset', session_id: 'obsolete', revision: 1000, topics: [TOPIC] });
|
||||
assert.equal(b.snapshot().revision, closedRevision, 'late old feed cannot revive its session');
|
||||
b.session = 'reloaded'; b.revision = 0; b.jobs.clear();
|
||||
const start = refreshZ2mTopology(b.hass, TOPIC);
|
||||
assert.equal(refreshZ2mTopology({ ...b.hass }, TOPIC), start);
|
||||
await start;
|
||||
assert.equal(b.callbacks.length, 2); assert.equal(b.live.size, 1);
|
||||
assert.equal(b.publications, oldPublications + 1);
|
||||
assert.equal(b.snapshot().states[KEY].phase, 'loading');
|
||||
assert.equal(b.snapshot().backendConnected, true);
|
||||
b.update(TOPIC, { phase: 'ready', result: map, obtained_at: 90 });
|
||||
assert.equal(b.snapshot().states[KEY].obtainedAt, 90, 'new session streams without remount');
|
||||
off(); assert.equal(b.cleanups, 2);
|
||||
});
|
||||
|
||||
test('#800 closed while subscribe ACK is pending cleans late ACK without releasing new feed', async () => {
|
||||
const b = backend(); b.subscribeGate = deferred();
|
||||
const oldAck = b.subscribeGate, off = b.observe(); await turn();
|
||||
b.emit({ kind: 'closed', session_id: b.session, revision: ++b.revision });
|
||||
b.session = 'reloaded'; b.revision = 0; b.jobs.clear(); b.subscribeGate = null;
|
||||
await refreshZ2mTopology(b.hass, TOPIC);
|
||||
assert.equal(b.callbacks.length, 2);
|
||||
oldAck.resolve(); await turn();
|
||||
assert.equal(b.live.size, 1); assert.equal(b.cleanups, 1);
|
||||
b.update(TOPIC, { phase: 'ready', result: map, obtained_at: 99 });
|
||||
assert.equal(b.snapshot().states[KEY].obtainedAt, 99);
|
||||
off(); assert.equal(b.cleanups, 2);
|
||||
});
|
||||
|
||||
test('#800 only current-session non-stale closed event can invalidate runtime', async () => {
|
||||
const b = backend(), off = b.observe(); await refreshZ2mTopology(b.hass, TOPIC);
|
||||
const before = b.snapshot();
|
||||
b.emit({ kind: 'closed', session_id: 'foreign', revision: 999 });
|
||||
b.emit({ kind: 'closed', session_id: b.session, revision: b.revision - 1 });
|
||||
assert.equal(b.snapshot().revision, before.revision); assert.equal(b.live.size, 1); off();
|
||||
});
|
||||
|
||||
test('#800 late command completion from closed session cannot erase new command deduplication', async () => {
|
||||
const b = backend(), off = b.observe(); await turn();
|
||||
const oldReply = deferred(); b.commandGate = oldReply;
|
||||
const oldStart = refreshZ2mTopology(b.hass, TOPIC); await turn();
|
||||
const oldEvent = { kind: 'state', session_id: b.session, revision: 10, provider: {
|
||||
topic: TOPIC, job_id: 'old', phase: 'loading', elapsed_ms: 100,
|
||||
} };
|
||||
b.emit({ kind: 'closed', session_id: b.session, revision: ++b.revision });
|
||||
b.session = 'reloaded'; b.revision = 0;
|
||||
const newReply = deferred(); b.commandGate = newReply;
|
||||
const newStart = refreshZ2mTopology(b.hass, TOPIC); await turn();
|
||||
oldReply.resolve(oldEvent); await oldStart;
|
||||
assert.deepEqual(b.snapshot().states, {}, 'late action result from unloaded session stays rejected');
|
||||
assert.equal(refreshZ2mTopology(b.hass, TOPIC), newStart, 'old finally must not delete new in-flight action');
|
||||
newReply.resolve({ ...oldEvent, session_id: b.session, revision: 1,
|
||||
provider: { ...oldEvent.provider, job_id: 'fresh', elapsed_ms: 0 } });
|
||||
await newStart;
|
||||
assert.equal(b.snapshot().states[KEY].jobId, 'fresh');
|
||||
assert.equal(b.calls.filter(c => c.type.endsWith('/start')).length, 2);
|
||||
assert.equal(b.callbacks.length, 2); off();
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ async def _start(client, transport, *, topic="zigbee2mqtt"):
|
||||
|
||||
|
||||
async def test_issue_800_background_15_minutes_two_clients_and_reopen(
|
||||
hass, hass_ws_client, transport, clock,
|
||||
hass, hass_ws_client, transport, clock, monkeypatch,
|
||||
):
|
||||
entry, coordinator = await _setup(hass)
|
||||
first, second = await hass_ws_client(hass), await hass_ws_client(hass)
|
||||
@@ -190,14 +190,24 @@ async def test_issue_800_background_15_minutes_two_clients_and_reopen(
|
||||
await second.close()
|
||||
await hass.async_block_till_done()
|
||||
assert len(transport.published) == 1 and not coordinator._listeners
|
||||
await _until(lambda: coordinator.snapshot("zigbee2mqtt")["provider"]["stage"] == "waiting")
|
||||
|
||||
clock.now += 600
|
||||
await hass.async_block_till_done()
|
||||
assert coordinator.snapshot("zigbee2mqtt")["provider"]["phase"] == "loading"
|
||||
clock.now += 300
|
||||
waiting = coordinator.snapshot("zigbee2mqtt")["provider"]
|
||||
assert waiting["elapsed_ms"] == 900_000 and waiting["phase"] == "loading"
|
||||
answer = transport.respond("zigbee2mqtt", waiting["job_id"])
|
||||
# Advance actual event-loop deadlines too, not only the displayed elapsed
|
||||
# counter. A reintroduced asyncio.timeout(600) must run and fail this witness.
|
||||
original_loop_time, clock_start = hass.loop.time, clock.now
|
||||
with monkeypatch.context() as timers:
|
||||
timers.setattr(hass.loop, "time", lambda: original_loop_time() + clock.now - clock_start)
|
||||
clock.now += 600
|
||||
for _ in range(4):
|
||||
await asyncio.sleep(0)
|
||||
await hass.async_block_till_done()
|
||||
assert coordinator.snapshot("zigbee2mqtt")["provider"]["phase"] == "loading"
|
||||
clock.now += 300
|
||||
for _ in range(4):
|
||||
await asyncio.sleep(0)
|
||||
waiting = coordinator.snapshot("zigbee2mqtt")["provider"]
|
||||
assert waiting["elapsed_ms"] == 900_000 and waiting["phase"] == "loading"
|
||||
answer = transport.respond("zigbee2mqtt", waiting["job_id"])
|
||||
await _until(lambda: not coordinator._operations)
|
||||
assert transport.listener_count == 0 and not transport.status_callbacks
|
||||
|
||||
@@ -462,11 +472,26 @@ async def test_issue_800_entry_unload_clears_jobs_and_new_entry_session_is_idle(
|
||||
):
|
||||
entry, coordinator = await _setup(hass)
|
||||
client = await hass_ws_client(hass)
|
||||
subscription = await _command(client, "subscribe")
|
||||
assert subscription["success"]
|
||||
assert (await client.receive_json())["event"]["kind"] == "reset"
|
||||
await _start(client, transport)
|
||||
previous_revision = coordinator.revision
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert coordinator.closed and not coordinator._jobs
|
||||
assert transport.listener_count == 0 and not transport.status_callbacks
|
||||
async with asyncio.timeout(1):
|
||||
while True:
|
||||
event = (await client.receive_json())["event"]
|
||||
if event["kind"] == "closed":
|
||||
break
|
||||
assert event == {"kind": "closed", "session_id": coordinator.session_id,
|
||||
"revision": previous_revision + 1}
|
||||
# The same live socket can retire its old feed, then attach to the next
|
||||
# loaded entry. Receiving closed never publishes an automatic replacement.
|
||||
await client.send_json_auto_id({"type": "unsubscribe_events", "subscription": subscription["id"]})
|
||||
assert (await client.receive_json())["success"]
|
||||
refused = await _command(client, "subscribe")
|
||||
assert refused["success"] is False and refused["error"]["code"] == "not_ready"
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
@@ -475,6 +500,13 @@ async def test_issue_800_entry_unload_clears_jobs_and_new_entry_session_is_idle(
|
||||
assert replacement.session_id != coordinator.session_id
|
||||
assert replacement.initial_events()[0]["topics"] == []
|
||||
assert len(transport.published) == 1
|
||||
assert (await _command(client, "subscribe"))["success"]
|
||||
reset = (await client.receive_json())["event"]
|
||||
assert reset == {"kind": "reset", "session_id": replacement.session_id,
|
||||
"revision": 0, "topics": []}
|
||||
restarted = await _start(client, transport)
|
||||
assert restarted["session_id"] == replacement.session_id
|
||||
assert len(transport.published) == 2
|
||||
|
||||
|
||||
async def test_issue_800_missing_mqtt_is_optional_and_reports_unavailable(
|
||||
|
||||
Reference in New Issue
Block a user