Files
houseplan-card/docs/reviews/CODE-REVIEW-550-r1.md
T
2026-09-13 09:55:03 +00:00

14 KiB
Raw Blame History

CODE-REVIEW-550-r1

Issue: #550 «Мутационный гейт: отличать срабатывание теста от ошибки сборки и подготовки» Материал ревью: d89b55ccf0bba40b51d713f880515b6f17a641a0 (ветка issue/550-mutation-outcomes) Диапазон: 43ba198d..d89b55cc (два коммита: 4d8ad3db, d89b55cc) Трек: инфраструктурный (§1 PROCESS.md) — ни одного файла класса A, вход сразу на S7-code-review. Заход: r1 · блокирующих циклов израсходовано 0 из 4.

Скоуп

Диапазон трогает только класс B/C: scripts/mutation-gate.mjs, scripts/mutation-gate-report.mjs, новый scripts/mutation-guard-outcome.mjs, test/mutation-gate.test.mjs, test/mutation-gate-report.test.mjs, новый test/mutation-guard-outcome.test.mjs, docs/TESTING.md. src/**, custom_components/** не тронуты — User-Visible: no на обоих коммитах верно, changelog не требуется.

Риск из тела issue: runMutant трактовал любой ненулевой exit гарда как «assertion killed», хотя многие гварды — цепочка подготовка && ... && oracle, и падение подготовки (компиляция, сборка, collection теста) не доказывает, что заявленный тест вообще исполнился.

Как проверялось

Дешёвые гейты подтверждены на этом SHA без повторного прогона (см. вводную задачи): npx tsc --noEmit, npm test, npm run build — Validate 34750198536 и push-Validate 34750061003, оба на точном d89b55cc, success.

Дополнительно проверено мной, потому что диапазон заявляет защитный AC:

Гейт Статус Почему
node scripts/smoke-select.mjs --base origin/dev --head HEAD прогнан диапазон не трогает src/**, инструмент подтвердил: «browser-smoke этим диффом не выбираются» — смоки не нужны
node --test test/mutation-guard-outcome.test.mjs прогнан локально зелёный, 9/9
check-docs.mjs, golden:verify, pytest tests_backend, model-invariants не прогнаны диапазон не трогает src/**, рендер, геометрию, Python — гейты не относятся к диффу
perf-профили не прогнаны не названы в AC, диапазон не трогает рендер/interaction пути

Проверка «тест умеет падать» для защитных AC — не поверила заявлению автора, а воспроизвела три новых мутанта из MUTANT_DEFINITIONS вручную, патчами по тем же якорям, что в реестре, и убедилась, что суит краснеет:

  1. mutation-outcome-setup-counts-as-assertion (патч if (false && phase === 'setup') в mutation-guard-outcome.mjs) → node --test test/mutation-guard-outcome.test.mjs: 8 pass / 1 fail (тест mutant-induced compile failure is setup, not a killed assertion красне­ет, потому что без раннего return для setup-фазы TS-ошибка мутанта не совпадает ни с ASSERTION_EVIDENCE, ни с ORACLE_SETUP_FAILURE, и попадает в ASSERTION_KILLED по умолчанию — именно та путаница, которую AC1-AC3 запрещают).
  2. mutation-ledger-accepts-setup-proof (патч validProof в mutation-gate.mjs, чтобы принимать 'setup') → node --test --test-name-pattern="#550 fixture: clean setup|#481 AC3" test/mutation-guard-outcome.test.mjs test/mutation-gate.test.mjs: 1 pass / 1 fail (assert.throws(() => recordCaught(..., 'setup'), /недоказанный outcome/) перестаёт бросать).
  3. mutation-report-setup-as-unparsed (патч if (false && asUnverifiable...) в mutation-gate-report.mjs) → node --test --test-name-pattern="#550: setup/invalid/interruption" test/mutation-gate-report.test.mjs: 1 fail (unverifiable.length 1 !== 4 — без ветки парсера три строки уходят в unparsed, отчёт больше не называет недоказанные исходы явно).

После каждой проверки файл восстановлен (git status --short — чисто).

Все три мутанта также подтверждены на CI, причём двумя разными dispatch-прогонами (Validate с mutants=true, что и требует PROCESS.md как единственное валидное доказательство, а не push-прогон):

  • Прогон на материале ревью 34750198536 (SHA d89b55cc): shard 2/6 поймал mutation-outcome-setup-counts-as-assertion, shard 1/6 — mutation-ledger-accepts-setup-proof, shard 5/6 — уже существующий ledger-written-at-end-only (регрессия, которую чинит второй коммит захода).
  • Предыдущий прогон 34749818213 (SHA 4d8ad3db, красный целиком из-за ledger-written-at-end-only) поймал mutation-report-setup-as-unparsed в shard 1/6 до точки отказа — это легитимное переиспользование через per-shard ledger-кэш Actions (третий мутант не нуждался в повторном прогоне на d89b55cc, потому что его файлы отпечатка не изменились между двумя коммитами захода).

Итого: 705 определений в реестре, диффом затронуто 35 (по файлам) / 5 (по определениям изменённых мутантов), все относящиеся к диффу свидетели пойманы на точном материале ревью — конвейер подтверждает это сам job'ом «Доказательство выполненных проверок» (success).

Таблица «чем краснеет» (#435) по AC issue

AC (из тела issue) Чем доказан Чем краснеет
Fixtures на все пять исходов (setup/invalid/compile/assertion/survived/timeout) получают честный outcome test/mutation-guard-outcome.test.mjs (9 тестов, включая regression-фикстуры из d89b55cc для subtest-вывода Node) Снятие любой из веток classifyCommandResult/interruption ломает соответствующий фикстурный тест — проверено вручную для setup-ветки (см. выше, п.1), остальные ветки покрыты симметрично тем же файлом
Guard tsc && test не сообщает исполнение named test, если tsc остановился раньше mutation-outcome-setup-counts-as-assertion (мутант в реестре) + фикстура «mutant-induced compile failure is setup» Воспроизведено вручную: без early-return для phase==='setup' тест краснеет (8/9, см. п.1); также поймано в CI на материале ревью
Ledger/report/reuse не переносят setup failure как подтверждение mutation-ledger-accepts-setup-proof + mutation-report-setup-as-unparsed, #481 AC3 (recordCaught бросает на proof='setup', readLedger не пускает старую/недоказанную схему) Оба мутанта воспроизведены вручную (п.2, п.3) и пойманы в CI
Существующие правомерные witnesses сохраняются, без рефактора всех определений Прочитано: в scripts/mutation-gate.mjs ни один существующий мутант не декларирует oracle: 'compile' (grep oracle: — совпадений в определениях нет), правки реестра ограничены тремя новыми записями плюс точечные патчи двух #481-мутантов под новую форму ledger Не защитный AC (ограничение объёма правки), доказательство — чтением диффа scripts/mutation-gate.mjs, не исполнением

Прочитано, но не переисполнялось

  • runGuardPhases/guardPhases/classifyCommandResult в mutation-guard-outcome.mjs — логика разбора &&-цепочки, регэкспы ORACLE_SETUP_FAILURE/ASSERTION_EVIDENCE: проверено чтением плюс целевым исполнением (см. выше), отдельно проверил, что guardNeedsTestBuild/ guardNeedsBundle не пересекаются с гипотетическим oracle: 'compile' гвардом (такой гвард — одна tsc-команда без node --test/demo/, обе проверки не сработают) — проверено чтением, не исполнением, живых мутантов с oracle: 'compile' в реестре пока нет.
  • buildTestBuild/buildBundle в mutation-gate.mjs теперь бросают на ненулевом статусе вместо игнорирования — согласуется с идеей задачи (tsc в подготовке мутанта — не оракул); поймано в CI как часть штатного прогона мутантов (ни один существующий мутант не полагался на старое поведение «код выхода tsc игнорируется»), отдельного мутанта на этот конкретный узел не заводили — риск низкий, узел прикрыт тем, что почти все гварды с test-build реально запускают его через buildTestBuild в каждом прогоне.
  • sh() получил timeout: 180_000 и killSignal: 'SIGTERM' — проверено чтением; не нашёл гварда, который перемножает несколько долгих команд под одним таймаутом (полный npm test/npm run build как гвард в реестре не встречается), поэтому 180 с — не риск ложных infrastructure-interruption.
  • LEDGER_SCHEMA bump 1→2 и инвалидация старых записей — намеренный сброс кэша, не миграция данных; чтением подтвердил, что readLedger отбрасывает и чужую схему, и записи без proof (тест #481 AC3 это покрывает).

Находки

Нет. High — 0, Medium — 0, Low — 0.

Отдельно отмечу (не находка, а наблюдение без действия): splitAndChain не обрабатывает последний символ строки в цикле (index < text.length - 1), теоретически мог бы неверно закрыть кавычку, если строка гварда заканчивается ровно на кавычке перед && — в реестре таких гвардов нет, и хвост всё равно берётся простым slice, так что наблюдаемого дефекта нет; проверил на трёх адверсариальных примерах (см. журнал проверки) — поведение корректно на всех.

Вердикт

Зелёный. Реализация закрывает риск из тела issue: подготовка (tsc, rollup, fix-test-build) больше не может выдать себя за падение заявленного oracle, compile-time свидетель допускается только явным oracle: 'compile', ledger schema 2 fail-closed отбрасывает недоказанные и старые записи, ночной отчёт отдельно называет недоказанные исходы. Три новых мутанта воспроизведены и вручную, и по логам CI-прогона на точном материале ревью. Объём правки реестра соразмерен риску — рефактора всех 705 определений не случилось.


Материал раунда


Материал раунда

  • Ветка: issue/550-mutation-outcomes, коммит d89b55ccf0bb — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 9d43b0e649a50deea03989718d9e25fe200ccf6a
    git log --all --format='%H %T' | grep 9d43b0e649a5
    
  • Тело issue: ccb0c6233ab2eca52ebe2ee4560645f5d118d84b8981a8de76550f4937c61a87
  • Вердикт конвейера: green · High 0