16 KiB
Code review #225 — r2
- Issue: https://github.com/Matysh/houseplan-card/issues/225
- Spec: тело issue (лёгкий трек
small), зелёноеSPEC-REVIEW-225-r2.md - Reviewed branch:
issue/225-import-attachment-url-query - r1 code review: жёлтый · SHA
e59962f· документdocs/reviews/CODE-REVIEW-225-r1.md - Дельта этого раунда:
git diff e59962f..HEAD= коммит04945e1(«fix: reject absolute urls in the content resolver, register the mutants») - Base:
origin/devat5f000cb(ветка — линейное продолжение5f000cb, не ребейз:git merge-base HEAD origin/dev=5f000cb,origin/devушёл вперёд по несвязанному issue #226 своей веткой) - Reviewer: Claude, независимая сессия без контекста реализации
Вердикт
Зелёный · цикл r2/2 · High: 0 · Medium: 0.
Разбор — по дельте (§2.10 PROCESS.md, issue #214): r1 закончился жёлтым с
двумя находками в скоупе (M1 — резолвер доверял path даже при scheme/
netloc; M2 — заявленные мутанты не осели в реестре). Дельта e59962f..HEAD
— один коммит, касающийся только того же файла/теста/гейта, которые уже были
в скоупе задачи; не ребейз, контракт поведения не менялся, новая подсистема не
затронута — сокращённый разбор оправдан.
Обе находки закрыты правильно и это подтверждено не заявлением автора, а
независимой проверкой: извлёк литеральную логику _internal_path (плюс
sanitize_marker_id/sanitize_filename, _looks_internal) вне HA-харнеса и
прогнал её на всех AC-кейсах и на всех трёх мутантах из реестра — старая
(до фикса) версия резолвит https://evil.example/houseplan_files/files/m1/doc.pdf
как внутренний файл, новая — корректно возвращает None; каждый из трёх
зарегистрированных мутантов при применении к текущему коду переворачивает
хотя бы один из своих AC-кейсов (детали — «Закрытие раунда r1» ниже). CI
Validate зелёный на точном SHA 04945e1, job backend — 339 passed (было
335 на e59962f, +4 — ровно новый параметризованный тест на 4 варианта
абсолютного/protocol-relative URL). Дешёвые гейты (tsc, npm test
962/962, build + сверка трёх копий бандла, mutation-gate --check по
всему реестру, включая три новых id) прогнаны локально и зелёные.
Новых находок в этом раунде нет.
Скоуп дельты
Один продуктовый коммит 04945e1 (Issue: #225, User-Visible: no —
верно: это закрытие M1/M2, а не пользовательское поведение; пользовательский
эффект уже описан в changelog коммитом e59962f, changelog в 04945e1 не
трогается и не должен был):
custom_components/houseplan/import_export.py(+10/−1) — в_internal_pathперед тем как доверятьparsed.path, добавлена проверкаif parsed.scheme or parsed.netloc: return None; комментарий указывает на ревью r1/M1. Остальная функция (префиксы,sanitize_marker_id/sanitize_filename, посегментное сравнение) не тронута — совпадает с диффомe59962f..HEADпострочно;tests_backend/test_ha_import_export.py(+20, 1 тест, 4 кейса) —test_issue_225_absolute_url_never_resolves_onto_a_local_file: https+FILES_URL, https+CONTENT_URL с query, protocol-relative//, https+PLANS_URL;scripts/mutation-gate.mjs(+41, 3 записи) —internal-path-ignores-query,internal-path-trusts-foreign-host,internal-path-allows-traversal, все сguard: node scripts/backend-test-guard.mjs issue_225;- никаких изменений в
docs/CHANGELOG*,src/**, i18n, конфиге совместимости — соответствуетUser-Visible: noи тому, что коммит чинит находки код-ревью, а не меняет заявленное поведение.
Как проверялось
| Гейт | Результат |
|---|---|
npx tsc --noEmit |
pass |
npm test |
962/962 pass (включает дешёвую половину мутационного гейта — test/mutation-gate.test.mjs: уникальность якорей и существование guard-файлов для всех мутантов, включая три новых) |
npm run build + сверка трёх копий бандла |
pass, dist/custom_components/houseplan/frontend/demo/srv/assets побайтово совпадают, git status после сборки чист |
node scripts/mutation-gate.mjs --check (весь реестр, не только новые id) |
ok на всех записях — новые три не столкнулись якорями со старыми 20+ |
CI Validate на точном SHA 04945e1 (gh api .../commits/04945e1/check-runs, job backend id 96542792712) |
completed success; лог: «Backend unit tests (pure + HA harness) — 339 passed in 5.26s» (было 335 на e59962f, делта +4 совпадает с числом новых параметризованных кейсов) |
Независимая проверка «мутант умеет ловиться» — вынес _internal_path/_looks_internal/sanitize_marker_id/sanitize_filename в автономный скрипт (без HA) и прогнал: (а) новую версию на AC1/AC1-plan/AC4/AC4a/AC5-M1; (б) буквальные патчи всех трёх мутантов реестра против неё |
новая версия проходит все кейсы; все три мутанта переворачивают результат хотя бы одного своего AC-кейса — не эквивалентные патчи, не «зелёный тест ничего не проверяет» (детали ниже) |
python -m pytest tests_backend -q локально |
не прогонял — окружение ревью на py3.12 без homeassistant; test_ha_*.py тем же механизмом молча пропускается, что и у автора и у меня же в r1 (см. AGENTS.md, «Backend»). Заменено CI-прогоном на точном SHA плюс независимым исполнением чистой логики вне HA — то же покрытие, что использовалось в r1 |
browser demo/smoke_*.mjs (127 шт.), npm run golden:verify, performance-профили |
не прогонял — дельта не касается src/**/визуала/производительности, ни один AC их не называет; то же основание, что в r1 |
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 — urlsplit(url).path доверяет пути даже при scheme/netloc; https://evil.example/houseplan_files/files/m1/doc.pdf резолвится как локальный файл, хотя _looks_internal говорит «внешний» |
Добавлена проверка if parsed.scheme or parsed.netloc: return None перед использованием parsed.path |
custom_components/houseplan/import_export.py:364-366; тест test_issue_225_absolute_url_never_resolves_onto_a_local_file (4 кейса: https+FILES_URL, https+CONTENT_URL+query, //+FILES_URL, https+PLANS_URL) — все требуют _internal_path(...) is None и _looks_internal(...) is False. Независимо прогнал ту же логику вне HA: без проверки все 4 URL резолвятся в валидный локальный путь, с проверкой — все 4 дают None |
M2 — три мутанта из ТЗ (internal-path-ignores-query, internal-path-allows-traversal, плюс подразумеваемый мутант на M1) не зарегистрированы в scripts/mutation-gate.mjs, разовый ручной прогон не оставляет постоянной защиты |
Три записи добавлены в MUTANTS (scripts/mutation-gate.mjs:676-716): internal-path-ignores-query, internal-path-trusts-foreign-host (регрессия M1), internal-path-allows-traversal, все с guard: node scripts/backend-test-guard.mjs issue_225 |
node scripts/mutation-gate.mjs --check — все три анкера уникальны в текущем коде; test/mutation-gate.test.mjs (часть npm test, 962/962) проверяет то же плюс существование guard-файла. Прогнал патчи буквально: internal-path-ignores-query переворачивает 2/6 кейсов AC1 (варианты с #fragment) и оба кейса AC4a; internal-path-trusts-foreign-host переворачивает все 4 кейса нового теста M1; internal-path-allows-traversal переворачивает кейс plans/_/../x.svg?v=1 из AC4 (для files-ветки этот же мутант оказывается эквивалентным — sanitize_marker_id("..") == "misc" ловит traversal независимо от структурной проверки, поэтому реестр справедливо не утверждает, что он ловит все AC4-кейсы, только тот один, который у файловой ветки не защищён вторым эшелоном) |
Унаследовано из r1
Без повторной проверки в этом раунде — дельта их не касается — приняты выводы
из docs/reviews/CODE-REVIEW-225-r1.md (SHA e59962f):
- диагноз бага и контракт исправления (
urlsplit, query/fragment вне определения файла) совпадают с кодом; - данные пользователя не переписываются (
item["url"]не трогается); identity()(:1034-1039) не включаетstorageв ключ сравнения — смена классификацииexternal → internalдля канонических cache-busted ссылок не ломает сопоставление manifest-строк (AC6);- traversal-защита для случаев без
scheme/netloc(три из четырёх кейсов AC4) не ослаблена дропом query — сегментные проверки работают над тем же материалом, что и раньше; - AC6/AC7 (regression, plan-only idempotency) — diff
e59962fтолько добавляет тесты в конец файла, plan-only диапазон (:489-736) вне диффа; - trailers, ветка, оба changelog в коммите
e59962f— по правилам.
Дельта r1→r2 не касается этих участков кода (только резолвер и мутационный
реестр), поэтому пересчёт не требовался — см. «Скоуп дельты» выше, где
подтверждено, что diff e59962f..HEAD ограничен ровно тем, что было
находками r1.
Что проверено и корректно (в этом раунде)
- M1-фикс не задевает уже покрытые ветки: прогнал старую/новую версию
резолвера на AC1 (5+3 кейса), AC4 (4 кейса), AC4a (2 кейса) — результат
идентичен до и после добавления проверки
scheme/netloc, потому что ни один из этих URL её не содержит. _looks_internalне менялась (провереноgit diff— файл затронут только в_internal_path), поэтому согласованность двух функций, из-за расхождения которых родился исходный баг #225, теперь восстановлена в обе стороны: внутренний путь ⇒ обе функции согласны; внешний хост ⇒ обе согласны «внешний».- Мутант
internal-path-allows-traversalумышленно снимает два патча сразу; комментарийbecauseв реестре объясняет, что снятие одного из них (len(tail) != 2) для файловой ветки было бы эквивалентным мутантом — проверил это утверждение напрямую (см. таблицу выше), оно верно. - Trailers коммита
04945e1(Issue: #225,User-Visible: no) корректны: фикс не меняет наблюдаемое пользователем поведение (только закрывает внутреннюю рассогласованность двух приватных функций), поэтому отсутствие правки changelog — не пропуск. process-gate/pre-push пройден (CI jobprocess-gate: success на04945e1).
Чего не проверял
python -m pytest tests_backend -qв собственном окружении — та же причина, что в r1 (py3.12, безhomeassistant,test_ha_*.pyмолча пропускаетсяconftest.py). Компенсировано CI-прогоном на точном SHA (339 passed) и независимым исполнением чистой логики резолвера вне HA.- Полный (дорогой) прогон
node scripts/mutation-gate.mjsс пересборкой бандла в worktree для каждого из 20+ мутантов реестра — не требовался: задача не трогает остальные мутанты, а три новых проверены анкерным--checkи независимым буквальным прогоном патчей против чистой логики (эквивалент того, что делает дорогой прогон, без пересборки бандла на каждый). Дорогой прогон — предрелизный гейт (.github/workflows/ mutation-gate.yml), не гейт ревью (PROCESS.md §8 vs §2.7). - Browser-смоки (127 шт.),
npm run golden:verify, performance-профили — дельта не касаетсяsrc/**, визуала или производительности; то же основание, что в r1.
Лимит циклов
Код-ревью лёгкого трека — 2 цикла (§4). Это второй и последний цикл; вердикт зелёный, лимит не понадобился.