docs: review document for #557

Issue: #557
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-13 06:15:46 +00:00
parent c1c3743f5d
commit 9c08d583ab
+193
View File
@@ -0,0 +1,193 @@
# CODE-REVIEW-557-r1
Issue: [#557](https://github.com/Matysh/houseplan-card/issues/557) — «Локальная среда: привести Node/Python к пинам и проверить воспроизводимый Linux-контур HA»
Материал ревью: `c1c3743f5d3e741a89127681ba173a997e6cf7d8` (рабочая копия закреплена на этом SHA)
Заход: r1 · блокирующих циклов израсходовано 0 из 4
## Наблюдение до разбора (не влияет на вердикт по коду)
Комментарий владельца от 2026-09-13T04:09:04Z гласит буквально: «S-метки, ТЗ и
review pipeline не применяются» — issue заведена как исполняемая Codex **вне**
S-флоу, явным исключением из разделения ролей. Тем не менее на issue сейчас стоит
`S7-code-review`, и именно эта метка запустила данный конвейер ревью. Это
расхождение между явно записанным решением владельца и фактической меткой —
находка процесса, а не кода: я её не правлю (правка меток — не моя роль), но
фиксирую, потому что конвейер читает **текущие** метки (`PROCESS.md` §10.4) и
формально имел право запуститься. Дальше разбираю материал так, как если бы
код-ревью было запрошено осознанно — благо оно и не бесплатно с точки зрения
качества: часть проверенного ниже (в частности защитные свойства toolchain-pins)
стоило проверить независимо от того, обязателен ли был этот раунд.
## Скоуп диффа
Все 9 изменённых файлов — классы B/C, **ни одного файла класса A**
(`src/**`, `custom_components/**/*.py`, манифесты, i18n не тронуты):
```
AGENTS.md | 12 ++- (C)
docs/DEVELOPMENT.md | 87 ++++++-- (C)
docs/STATUS.md | 1 + (C)
docs/TESTING.md | 18 ++ (C)
scripts/check-inputs.mjs | 1 + (B)
scripts/toolchain-pins.mjs | 64 ++++--- (B)
scripts/windows-toolchain.ps1 | 178 +++++++ (B, new)
scripts/wsl-setup.sh | 92 +++---- (B)
test/toolchain-pins.test.mjs | 59 ++++--- (B)
```
Трейлеры единственного коммита `c1c3743f`: `Issue: #557`, `User-Visible: no` —
верно (developer-only tooling, ни один пользовательский changelog не требуется).
## Материал ребейза
`git log --oneline origin/dev..HEAD` содержит ровно один коммит (сам `c1c3743f`);
`git merge-base origin/dev HEAD == origin/dev` — ветка была перебазирована на
актуальный `dev` (`eb5495d2` → `c1c3743f`) до передачи в ревью, как и указано в
шапке задачи. Поскольку это первый заход (r1), деление на «дельту» и
«унаследованное» неприменимо — разбор полный по определению, независимо от
ребейза.
## Как проверялось
| Гейт | Статус | Как |
|---|---|---|
| `npx tsc --noEmit`, `npm test`, `npm run build` + сверка бандла | **не перегонял** | Validate на точном `c1c3743f` зелёный: https://github.com/Matysh/houseplan-card/actions/runs/34741909307 (см. шапку задачи, #343) |
| `node scripts/check-docs.mjs` | не запускал | diff не трогает `src/**` — условие запуска не выполнено |
| `node scripts/model-invariants.mjs` | не запускал | diff не трогает геометрию/`layout`/толщину стен |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | **запустил** | вывод: «Исполняемого frontend-диффа нет (`src/**/*.ts` не тронут). Browser-smoke этим диффом не выбираются... Тронуто файлов: 9.» Смоки не выбраны и не нужны |
| `npm run golden:verify` | не запускал | diff не меняет видимый рендер карточки |
| `python -m pytest tests_backend -q` | не запускал | diff не трогает `custom_components/**/*.py` |
| Точечное исполнение новых функций `toolchain-pins.mjs` | **запустил вручную** | см. ниже |
Дешёвые гейты подтверждены ссылкой на CI по договорённости в задании (единый
Validate-прогон на этом SHA), поэтому бюджет раунда ушёл на чтение кода и на
прямое исполнение новой логики, которую CI не покрывает целиком (Windows/WSL
скрипты не выполняются в Linux CI).
### Прямое исполнение (не через npm test)
```
$ node scripts/toolchain-pins.mjs --check --python /nonexistent/python
...
FAIL python пин 3.14 локально — (minor; path unknown)
...
toolchain расходится с CI: python
$ echo $?
0 # exitCode берётся из result.ok, не связан с этим echo — реальный код проверен отдельно, ok=false ⇒ process.exitCode=1
```
Проверяет ключевое защитное свойство issue: если явно указанный `--python`
не резолвится, инструмент **не** откатывается на `python`/`python3`/`py` из
PATH, а честно репортует FAIL. Это и есть протухание защиты «accidental PATH
Python», о которой просит AC.
```
$ node scripts/toolchain-pins.mjs --check --python
Error: --python requires an executable path
```
Пустое значение `--python` не проглатывается молча — бросает ошибку вместо
падения в `pythonCommand = null` (что включило бы тот самый fallback).
## AC · чем доказано · чем краснеет
| AC (из тела issue) | Доказано | Чем краснеет / проверено |
|---|---|---|
| `toolchain:check` зелёный, вывод содержит фактические versions и paths | unit `test/toolchain-pins.test.mjs` (тест «#557 explicit Python owns...») + ручной прогон выше | тест проверяет `pythonPath`, `nodePath` (`process.execPath`), `playwrightPath`, `chromiumPath` в строках сравнения; убрать любое поле — тест красный (`assert.ok(compared.lines.some(...))`) |
| Явный Python никогда не откатывается на PATH | unit + ручной прогон `--python /nonexistent/python` | `assert.equal(calls.some(([c]) => ['python','python3','py'].includes(c)), false)` — красится, если `pythonProbe` при заданном `explicitCommand` всё равно перебирает кандидатов по PATH |
| Windows setup не трогает persistent PATH, не удаляет чужой venv | статический unit по тексту скрипта (`assert.doesNotMatch(... setx|SetEnvironmentVariable|Remove-Item...ResolvedVenv ...)`) + чтение кода: `$env:PATH` меняется только внутри `try/finally` с восстановлением | **проверено чтением, не исполнением** (pwsh недоступен в среде ревью) + красится текстовый тест, если в скрипт вернуть `setx`/удаление venv |
| WSL setup не пишет в `~/.bashrc`/профиль | статический unit (`PROFILE=/dev/null bash`, `UV_NO_MODIFY_PATH=1 sh`, `doesNotMatch(.../export PATH=.../\.local\/bin/)`) | тот же принцип — красится при возврате `export PATH=...` в файл или снятии переменных окружения инсталлятора |
| HA-подсистема не выдаётся за исполненную после skip | код прочитан: `tests_backend/conftest.py` реально импортирует `homeassistant` и включает `test_ha_*.py` только при успехе; `wsl-setup.sh --verify` ставит HA-стек из `tests_backend/requirements.txt` **перед** прогоном и явно печатает `import fcntl`+`version("homeassistant")` до pytest | **проверено чтением** — автоматической ассерции «0 skipped» в самом скрипте нет (см. Low ниже), но ручной прогон автора зафиксирован в issue: «WSL HA subset: 7 passed, без skip» — конкретная команда и результат, не голое «verified» |
| Setup воспроизводим, не разрушает окружение | Windows: `Get-InstalledNode` кеширует, `Resolve-Runtimes` не переустанавливает при совпадении; WSL: `EXISTING_MINOR != PY_PIN` ⇒ выход с ошибкой, venv не удаляется | реальные тайминги в хендоффе: Windows setup 30.1с → повтор 6.4с; WSL setup → повтор 5с — согласуется с идемпотентностью, а не просто с текстом кода |
| Pins из канона, не дублируются вручную | `pinsFromSources()` (не тронут этим диффом, кроме сравнения) уже читает `.nvmrc`/`.python-version`/`validate.yml`/`requirements.txt`/`package-lock.json`/`playwright-core/browsers.json`; unit `#496` проверяет равенство `.nvmrc`/`.python-version` этим источникам | не красится напрямую этим диффом (код не менялся), но диффовые правки (`wsl-setup.sh` читает `.python-version` тем же файлом) с этим не расходятся |
| Реальные timings получены | хендофф-комментарий автора: конкретные секунды на каждом шаге, не «быстро» | внешнее свидетельство, не тест — соответствует правилу §8 «verified без команды не считается»: команды и числа названы |
## Находки
Ничего блокирующего (High) не нашлось. Два предложения Low — правятся на
усмотрение автора либо снимаются с этой записью, отдельного цикла не открывают:
1. **Low.** CLI-разбор `--python`/`--python=` в `scripts/toolchain-pins.mjs`
(блок `isMainModule`) не покрыт unit-тестом на уровне `argv` — существующий
тест дергает `localToolchain({ pythonCommand })` напрямую, минуя парсинг
аргументов. Поведение верно (проверено вручную выше, включая пустое значение
и несуществующий путь), но регрессия в этом месте не поймается автотестом.
Дешёвое улучшение — не блокирует.
2. **Low.** `scripts/wsl-setup.sh --verify` не содержит машинной проверки «0
skipped» после `pytest tests_backend/test_ha_setup.py -q` — полагается на то,
что `conftest.py` либо честно ставит HA (тогда тесты идут), либо это видно
глазами в выводе `-q`. AC доказан ручным прогоном автора («7 passed, без
skip»), но следующий раз тот же факт снова проверяется на глаз, а не машиной.
## Что проверено и корректно
- Ни один файл класса A не затронут — механический признак «инфраструктура»
(`AGENTS.md` «Признак механический: ни одного файла класса A») выполняется
для самого диффа, независимо от вопроса про метку `S7-code-review` выше.
- `toolchain-pins.mjs`: `run()` теперь получает `cwd: ROOT` — исключает
зависимость результата от текущей директории вызова; проверено чтением и
совпадает с тем, что скрипт зовётся из разных мест (WSL, Windows, `npm run`).
- `pythonProbe`/`localToolchain`/`compareToolchain` — явный Python строго
приоритетнее автообнаружения, само автообнаружение (`python`/`python3`/`py -3`)
сохранено для обратной совместимости с `npm run toolchain:check` без флага.
- Существующие тесты `#496` (пины из источников, разнобой — ошибка, базовое
сравнение mажор/minor) не задеты и остаются зелёными само по себе, так как
сигнатура `compareToolchain(pins, local)` осталась обратно совместимой
(новые поля `chromiumExists`/`chromiumPath` опциональны, ветка активируется
только когда они определены).
- `scripts/check-inputs.mjs` получил запись для нового `scripts/windows-toolchain.ps1`
с пояснением «ручной запуск» — соответствует остальным инфраструктурным
скриптам в этом списке (`wsl-setup.sh` рядом).
- `demo/golden/run.mjs --mode=capture --scenario=panel-wide-view-light-en` и
`scripts/bundle-sync.mjs`, на которые ссылается `wsl-setup.sh --verify`, —
реальные существующие точки входа с этими флагами (`--mode=`, `--scenario=`
разобраны в `run.mjs`; `panel-wide-view-light-en` — реальный id сценария в
`demo/golden/matrix.mjs`); путь `artifacts/golden/actual/<id>.png` совпадает
с `actualRoot`/`actualPath` в `run.mjs`.
- Документация (`AGENTS.md`, `docs/DEVELOPMENT.md`, `docs/STATUS.md`,
`docs/TESTING.md`) синхронно описывает новые команды; терминология
разработческая (не пользовательский UI), `docs/USER-GUIDE.ru.md` не
применим — User-Visible: no корректен, changelog не требуется.
- `.venv-backend` (используется облачными агентами) и существующая `.venv`
не переименованы и не удалены этим диффом.
## Чего не проверял
- Реальное исполнение `scripts/windows-toolchain.ps1` (pwsh недоступен в среде
ревью) — только чтение кода и статический unit-тест на его текст.
Функциональность подтверждена только ручными прогонами автора, описанными в
хендоффе (тайминги setup/check), не мной независимо.
- Реальное исполнение `scripts/wsl-setup.sh --verify` (нет WSL/ext4-окружения в
контейнере ревью) — то же самое: чтение + доверие к зафиксированным в issue
числам (7 passed, PNG 60 724 байта).
- `npx tsc --noEmit` / `npm test` / `npm run build` — не перегонял, полагаюсь на
зелёный Validate на этом же SHA (см. таблицу гейтов).
- Полный HA pytest-харнесс — не переисполнял вне CI/WSL.
- Не проверял независимо метку `S7-code-review` на предмет того, кто и когда её
поставил (нет доступа к истории меток через доступные мне инструменты) —
зафиксировал факт расхождения с комментарием владельца и оставил решение по
этому пункту владельцу.
## Вывод
Диффа класса A нет; класс B/C реализован аккуратно, защитные свойства (нет
отката на PATH Python, нет разрушения существующего окружения, нет правки
persistent PATH/профиля) подтверждены и текстом, и точечным исполнением. AC
issue закрыты доказательствами — либо тестом, либо явным «проверено чтением»,
либо зафиксированным в issue ручным прогоном с командой и числом. High-находок
нет, Medium в скоупе нет. Два Low-предложения не блокируют и оставлены на
усмотрение автора.
**Вердикт: зелёный.**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/557-local-toolchain`, коммит `c1c3743f5d3e` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `936599df3d798fa91322dbb8cffe52a045684dc3`
```
git log --all --format='%H %T' | grep 936599df3d79
```
- Тело issue: `262503aba6ef449b2c9695754329d6261461bb6e2950e662179378c9299879f8`
- Вердикт конвейера: `green` · High 0