WinClean v2.20: correctness and honesty round - #10
Conversation
…т больше не врёт
БЕЗОПАСНОСТЬ (MyAI-75us.7). Test-PathProtected сравнивал ТЕКСТ, а GetFullPath
не раскрывает reparse point. Измерено на живой ФС: junction с невинным путём,
указывающий в Program Files, проходил проверку, а Get-ChildItem по нему
перечислял содержимое ЦЕЛИ - дальше удалялись реальные файлы. Проверено до и
после фикса на этой машине: было False (чистим), стало True (отказ), и junction
действительно показывал 1cv8/7-Zip/ACD Systems.
Границы уточнены замером, а не рассуждением: вложенные ссылки безопасны и так
(Get-ChildItem -Recurse внутрь reparse point не заходит, Remove-Item удаляет
ссылку и не трогает цель), поэтому чинится только КОРЕНЬ обхода. Ссылка на
безобидную цель по-прежнему чистится - иначе "безопасно" достигалось бы отказом
всем, у кого кэш вынесен на другой диск.
ТИХИЕ ОТКАЗЫ (MyAI-75us.8), четыре места рапортовали успех:
- npm: exit code не читался вовсе, при EPERM печаталось "npm cache cleaned";
- журналы событий: полный провал перечисления давал "cleared (0 logs)" SUCCESS;
- privacy: Remove-Item -EA SilentlyContinue не бросает, поэтому catch был мёртв,
а "cleared" дописывалось безусловно. Теперь успех подтверждается сверкой
количества значений до/после (вынес Get-RegistryValueCount);
- winget source update: проверялось завершение job, но не его exit code.
ГЕЙТ (MyAI-75us.12):
- "in sync with origin" проверялось по ЛОКАЛЬНОМУ remote-tracking ref без fetch,
то есть подтверждало состояние, которого могло уже не быть. Теперь fetch с
проверкой кода возврата, сравнение с @{upstream}, fail-closed при недоступном
remote;
- версия Pester не была ограничена, хотя CI ограничивает сверху. Pester 6.0.1
уже вышел, любой Install-Module увёл бы гейт на мажор. Заведён общий
tools/Invoke-Tests.ps1 (по образцу Invoke-Lint.ps1): одна точка истины про
версию, путь и правило "пропущенный тест = провал", её зовут И CI, И гейт.
ТЕСТЫ, КОТОРЫЕ НЕ МОГЛИ УПАСТЬ (MyAI-75us.13): в двух тестах логирования
единственный Should был спрятан за if (Test-Path $log) - то есть тест проходил
ровно при том дефекте, ради которого написан. Обе проверки сделаны жёсткими;
мёртвая $sizeBefore наконец используется. Мутационно проверено: обе правки
ловят соответствующую поломку продукта.
376 -> 386 тестов.
…аг -SkipDiskCleanup STORAGE SENSE (MyAI-75us.2). Задача искалась по пути \Microsoft\Windows\DiskCleanup\, где её нет (там SilentCleanup). Реальная лежит в \Microsoft\Windows\DiskFootprint\. Ветка была НЕДОСТИЖИМА на любой машине, каждый прогон уходил в legacy cleanmgr: на чистой VM это 10 секунд, на рабочей станции - 901 секунда из 1101 (замер по логу), причём он не успевал завершиться. Теперь поиск по имени, без хардкода пути. 🔴 Одной правки пути было мало. Успехом считался факт запуска: задача, стартовавшая и мгновенно упавшая, писала "Storage Sense completed", и cleanmgr ПРОПУСКАЛСЯ - то есть машина осталась бы непочищенной, а прогон отрапортовал успех. Проверено на живой машине, где эта задача возвращает 0x80040154. Теперь: сравнение с СОБСТВЕННЫМ прошлым LastRunTime задачи (а не с wall-clock), явная обработка отказа запуска и fail-closed по LastTaskResult - ненулевой результат уводит в cleanmgr. СБРОС СОСТОЯНИЯ (MyAI-75us.11). v2.19 сбрасывала три массива и счётчик шагов, при этом комментарий обещал, что случай "dot-source + два вызова" обработан. Переживали вызов: освобождённые байты, категории, счётчики предупреждений и ошибок, RebootRequired, Aborted и StartTime - второй прогон описывал оба сразу и считал длительность от загрузки скрипта. Вынес New-RunStats: одно определение, используется и при загрузке, и на старте каждого прогона. ЛОГИРОВАНИЕ И RESULT JSON (MyAI-75us.9): - отказ записи лога больше не проглатывается: латч, одна консольная строка (через Write-Host - Write-Log ушёл бы в рекурсию) и новое поле LoggingDegraded в result JSON, чтобы потребитель знал, что лог неполон; - у Write-ResultJson комментарий гласил "Must be loud", а код ставил WARNING, при том что exit-код считается по ErrorsCount: прогон без запрошенного артефакта завершался кодом 0. Теперь ERROR. Тест, закреплявший старое поведение, изменён осознанно. ФЛАГ -SkipDiskCleanup (MyAI-75us.4): пропустить ТОЛЬКО Storage Sense / Disk Cleanup. Раньше единственным способом был -SkipCleanup, гасящий вообще всю очистку ради одного шага. Паритет с get.ps1 подхватился drift-guard'ом из v2.19. 386 -> 392 теста.
… Delivery Optimization
ТАЙМАУТ CLEANMGR (MyAI-75us.5), два дефекта в одном месте:
- блок finally стирал StateFlags СРАЗУ после решения "оставим доделывать в фоне",
то есть вынимал конфигурацию из-под работающего процесса. Гадать, читает ли
cleanmgr флаги лениво или разом на старте, во время его elevated-цикла удаления
не стоит - теперь при живом процессе сметание пропускается, флаги подберёт
собственное сметание следующего прогона (ровно тот leftover-случай, ради которого
оно и добавлялось в v2.16);
- сам таймаут логировался как INFO. Убивать cleanmgr по-прежнему хуже, но следствие
не проговаривалось: всё измеренное дальше - частичное, а прогон печатает итог и
пишет JSON, пока elevated-процесс продолжает удалять. Теперь WARNING и новое поле
DiskCleanupPending в result JSON: TotalFreedBytes при нём - нижняя оценка.
DELIVERY OPTIMIZATION (MyAI-75us.6): предупреждение "nothing freed, N still present"
срабатывало на ЗДОРОВОЙ системе. Замер покрывает всю папку DO, а штатный cmdlet
удаляет только кэш контента: логи и состояние службы остаются и удалять их не наше
дело. Значит "размер не изменился" после успешного cmdlet - не признак отказа.
Воспроизводилось на EN-стенде дважды подряд и выбивало прогон за бюджет
предупреждений. Понижено до DETAIL с честной формулировкой; настоящий отказ
по-прежнему виден - cmdlet вызывается с -ErrorAction Stop и уходит в catch.
🔴 ОСОЗНАННО НЕ ТРОНУТО: одноимённое предупреждение в Remove-FolderContent
("Update logs - nothing freed ... probably locked") оставлено WARNING. Там кандидаты
реально отобраны и удаление реально не удалось - это честный сигнал (фикс v2.16),
а не шум.
ЗАМЕРЫ БРАУЗЕРОВ (MyAI-75us.10): дельта считалась через Get-FolderSize, который
возвращает 0 и при пустой папке, и при отказе доступа. Недоступный ПОСЛЕ-замер
превращался в "освободили всё, что намеряли до". Теперь checked-замер: если хоть
одна папка не измерилась, дельта не считается и байты не приписываются.
392 -> 393 теста. Два теста обновлены осознанно (они закрепляли прежнее поведение).
…а в инфраструктуре стенда MyAI-75us.14: внутренние guard'ы Clear-DeveloperCaches/Clear-DockerWSL/Clear-VisualStudio проверяли только свой частный флаг, хотя диспетчер гасит фазу по ($SkipCleanup -or флаг). Через Start-WinClean они недостижимы, но именно их получает прямой вызов дот-сорснутой функции - и там -SkipCleanup не работал вопреки документированному контракту. MyAI-75us.15: Join-Path собирается посегментно (обратный слэш внутри аргумента на Linux - обычный символ, не разделитель), смоук берёт [System.IO.Path]::GetTempPath() вместо $env:TEMP. Ни один из этих скриптов на Linux сегодня не запускается (на хост деплоятся только четыре других), поэтому это гигиена, а не живой дефект. MyAI-75us.17: Deploy-StandRunner больше не игнорирует код возврата ssh - иначе неудачное удаление старых конфигов проходило молча и ночная матрица гоняла бы устаревшую VM. New-StandVM проверяет exit code msiexec (1603 кладёт частичную установку, а Test-Path на pwsh.exe при этом проходит) и реально запускает pwsh, требуя >= 7.1.
…становления MyAI-75us.16: частота точек восстановления чинилась ТОЛЬКО когда родитель убивал дочерний процесс. Дочерний, завершившийся штатно после провала собственного finally, приводил к безусловному снятию маркера - значение оставалось прижатым к 0 бессрочно, и не оставалось ничего, что заставило бы следующий прогон повторить попытку. Теперь восстановление вызывается на обоих путях: функция идемпотентна (при значении != 0 не делает ничего), поэтому проверка на штатном пути бесплатна и превращает предположение в факт. Версия 2.20 проставлена в 9 местах, CHANGELOG заполнен, документация обновлена: -SkipDiskCleanup в обеих README и в схеме JSON, новые поля LoggingDegraded и DiskCleanupPending в docs/result-json.md, модель защиты путей от junction в docs/safety.md. 🔴 Тесты версии сами были сломаны: регекс 2\.1[3-9] переставал совпадать на 2.20, то есть они упали бы на самом бампе, а не на дефекте. Переписаны на сравнение версий как версий + проверку инварианта "PSScriptInfo и $script:Version совпадают". 393 -> 394 теста. Гейт: 8 из 8 содержательных проверок зелёные.
💡 Codex ReviewLines 1451 to 1455 in 1f31c0b On the supported PowerShell 7.1 runtime, ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…ов был сломан 🔴 Get-RegistryValueCount отображал "отказано в доступе" в 0, то есть воспроизводил ровно тот дефект, ради которого писался: неудачное удаление плюс нечитаемая проверка после = "очищено". Теперь три состояния: 0 (нет ключа или он пуст), N (прочитан), $null (есть, но не читается). Вызывающий больше не считает неизвестность нулём: и до, и после удаления неизвестность даёт предупреждение, а не тихий успех. RunMRU присоединён к общему списку с проверкой: он остался на старом пути и по-прежнему рапортовал успех по счётчику ДО удаления. Delivery Optimization: формулировка была противоположной ошибкой - "cache cleared" утверждает то, чего неизменившийся размер папки не доказывает. Теперь сообщается только известное: cmdlet отчитался успехом, столько-то осталось на диске. Утечка StateFlags: ветка cleanmgr намеренно не сметает флаги при живом процессе, а Storage Sense возвращается раньше этой ветки - значит после брошенного прогона флаги могли остаться навсегда. Сметание вынесено в начало функции и идёт на любом пути. Гейт: код возврата git rev-list не проверялся, а $ahead/$behind инициализированы нулями - сломанный ref прочитался бы как "в синке". Та же форма лжи, ради устранения которой проверка и переписывалась. Storage Sense: при нескольких задачах с этим именем брали первую попавшуюся; теперь это повод не гадать, а уйти в cleanmgr. Тест на третье состояние счётчика добавлен (через Mock, без порчи реестра машины). 394 -> 395 тестов.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c946bc69a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| $script:Stats.FreedByCategory["Developer"] += $freed | ||
| Write-Log "npm cache cleaned - $(Format-FileSize $freed)" -Level SUCCESS | ||
| } elseif ($npmExit -ne 0) { |
There was a problem hiding this comment.
Honor npm failures before reporting success
When npm cache clean --force exits non-zero after deleting only some entries (for example EPERM on one locked cache file), $freed is positive, so the preceding success branch logs the cache as cleaned and skips the direct-removal fallback; this elseif only runs for failures that freed nothing. That still hides a partial npm cleanup and leaves remaining cache entries behind without a warning, so the non-zero exit should be handled before reporting success or should warn/fallback regardless of the delta.
Useful? React with 👍 / 👎.
| Get-ChildItem -Path $regPath -ErrorAction SilentlyContinue | ForEach-Object { | ||
| Remove-ItemProperty -Path $_.PSPath -Name "StateFlags$sageset" -Force -ErrorAction SilentlyContinue | ||
| } |
There was a problem hiding this comment.
Avoid sweeping flags while cleanmgr may still be active
If a previous run hit the new cleanmgr timeout, this version deliberately leaves that elevated process running in the background; starting another run while it is still active reaches this unconditional pre-sweep and removes StateFlags9999 from every handler before the old cleanmgr exits. That reintroduces the exact in-flight configuration removal the later finally now avoids, so the leftover sweep needs to be deferred or gated when a prior cleanmgr is still running.
Useful? React with 👍 / 👎.
…бует доказательств 🔴 КРИТИЧЕСКОЕ (нашло ревью, воспроизведено замером). Мой же фикс junction проверял ТОЛЬКО последний сегмент пути. Путь, у которого junction является ПРЕДКОМ, атрибута reparse на листе не имеет, GetFullPath его не раскрывает, текстовое сравнение не срабатывает: <junction>\Windows возвращал False при 120 реальных детях C:\Windows, видимых через ссылку. Введена Resolve-PathThroughLinks - разбор ссылок на ЛЮБОМ уровне с перезапуском (раскрытая цель сама может лежать под ссылкой) и ограничителем циклов. Добавлены два теста: путь через junction в защищённое отвергается, глубокий путь под безобидной ссылкой - нет. Плюс fail-open там же: любой отказ Get-Item сводился к $null и падал в "не защищён". Теперь как в Get-FolderSizeChecked - "не найдено" это ответ, всё остальное нет. 🔴 Storage Sense: exit code 0 не является доказательством очистки. Когда Storage Sense выключен в Параметрах, задача стартует, не делает ничего и выходит с нулём - мы бы погасили все 23 обработчика cleanmgr и освободили ноль, отрапортовав успех. Ровно тот класс, ради которого делается релиз, и он стал ДОСТИЖИМ именно потому, что я оживил эту ветку. Теперь успех признаётся только при измеримом приросте свободного места. npm: exit code проверяется ПЕРВЫМ. Прежний порядок означал, что частичный отказ (освободили 400 МБ, потом EPERM) рапортует успех и пропускает fallback - код возврата читался и игнорировался ровно там, где он важен. Write-Log: после отказа writer сбрасывается, иначе живой guard держал мёртвый объект и все последующие строки молча терялись - пустой catch просто переехал бы со второй строки. Dispose в Start-WinClean обёрнут: исключение оттуда выходило из функции, и проверка exit-кода не выполнялась вовсе. Сметание StateFlags в начале функции гейтится по живому процессу cleanmgr - иначе оно воспроизводило ту самую правку, которую делает finally. Тест журналов теперь доказывает, что Mock перехватил, ДО вызова продукта: иначе при любом сбое перехвата он вычистил бы реальные журналы машины разработчика. CHANGELOG: убран over-promise про Storage Sense - на машине, где мерили 901 секунду, задача находится, но падает, и время там снимает флаг, а не этот фикс. 395 -> 397 тестов.
Три задачи эпика MyAI-75us (.19 тихие отказы, .20 замеры и гонки, .21 тесты). Каждая находка сверена с кодом перед правкой, одна отклонена как уже закрытая на месте вызова (Get-RecycleBinSize: пустота решается по счётчику элементов). Тихие отказы: - winget source update: незапускаемый exe оставлял $LASTEXITCODE неустановленным, и guard коротил на null - единственный молчащий путь в блоке - журналы событий: решение принималось по ОТФИЛЬТРОВАННОМУ списку, поэтому 40 читаемых каналов из 510 при 470 ошибках давали чистый SUCCESS - npm: обе стороны замера через Get-FolderSize, который отвечает 0 и на "пусто", и на "нечитаемо" - тот же дефект, что закрыт для браузеров в этом же релизе Замеры и гонки: - браузеры: "до" мерил сырой обходчик, "после" - проверяющий, то есть вычитались разные множества файлов; теперь попарно по путям одной функцией - точка восстановления: Kill($true) возвращается до смерти дерева процессов - Invoke-Tests/гейт: файл, упавший на дискавери, не попадал ни в один счётчик, и CI зеленел с молча отсутствующим тест-файлом (измерено на Pester 5.7.1) Storage Sense (главный пробел релиза, тестов не было вообще): - три решения вынесены в чистые функции Select-StorageSenseTask / Get-StorageSenseVerdict / Wait-StorageSenseTask и покрыты 16 поведенческими тестами; ожидание больше не требует планировщика и двух реальных минут - путь задачи закрепляется, повторные поиски не могут подменить задачу - исчезнувшая задача отличается от таймаута (раньше печаталось "не уложилась в 120 секунд" после пяти) Независимое ревью Codex нашло четыре дефекта уже в этих правках, все исправлены: обрезка по нулю на каждом пути завышала освобождённое место, результат WaitForExit(5000) игнорировался, предупреждение на любую ошибку перечисления грозило хроническим шумом, -TaskPath $null бросает binding-ошибку, которую -ErrorAction не гасит. 397 -> 426 тестов. Новые тесты проверены мутациями: снятие требования прироста места, схлопывание "исчезла" в "таймаут", выбор первой из одноимённых задач - все три пойманы, файл каждый раз восстановлен по хешу.
…я про вечное предложение обновления MyAI-75us.18 и .22 - последние два пункта, которые можно закрыть без стенда. .18 Разбор размера (ConvertFrom-HumanReadableSize) Утверждение Codex #15 сверено с кодом и подтвердилось: "1,234 KB" - обычная en-US запись тысяч и ровно то, что отдаёт shell для элементов корзины, когда точное свойство размера недоступно - читалось как 1.234 KB. Занижение в 1000 раз. Старое правило: одиночный разделитель всегда десятичный. 🔴 Очевидное лечение оказалось бы хуже дефекта, это измерено на .NET: AllowThousands НЕ проверяет форму группировки, поэтому разбор по текущей культуре читает "1,5" как 15 на en-US. То есть занижение в 1000 раз сменилось бы завышением в 10 и сломало бы существующий тест "1,5 GB". Теперь сначала проверяется ФОРМА (1-3 цифры, затем группы ровно по 3), и культура спрашивается только для строки, которую честно можно прочесть двояко. Культура инжектируется параметром, поэтому правило тестируется без смены локали машины. .22 Документация docs/troubleshooting.md: почему одно и то же приложение предлагается к обновлению каждый прогон. Две причины, обе воспроизведены вживую: winget отказывается управлять пакетами user scope из повышенного процесса (а WinClean требует повышения), и установщик, пишущий в свою же запись деинсталляции устаревшую версию, делает предложение вечным - winget сравнивает именно её, а не установленный exe. Приведена read-only диагностика и оба выхода. Кодом это не чинится, но винят за это нас. 426 -> 433 теста. Новые проверены мутациями: возврат старого правила ловится тремя тестами, снятие проверки формы - тем, который написан против худшего лечения.
Проверка PR #10 перед выпуском: четыре специализированных агента pr-review-toolkit + кросс-движковое ревью Codex. Стенд к этому моменту уже был зелёным на обеих машинах, и всё равно нашлось следующее. 🔴 БЛОКЕР. Get-StorageSenseVerdict падал на КАЖДОМ реальном коде отказа. LastTaskResult имеет тип UInt32; у любого HRESULT-отказа поднят старший бит, поэтому 0x80040154 приходит как 2147746132 и приведение к [int] бросало исключение. Оно уходило из Invoke-StorageSense, фаза DeepSystemCleanup помечалась как Failed, Clear-WindowsOld не выполнялся вовсе. Найдено НЕЗАВИСИМО двумя ревьюерами и подтверждено измерением на рабочей станции. Мой тест был зелёным ИМЕННО ИЗ-ЗА дефекта: PowerShell разбирает литерал 0x80040154 как Int32 -2147221164, поэтому приведение проходило. Теперь разбор через [long]::TryParse, а тест передаёт ([uint32]2147746132) и проверяет тип. Остальные подтверждённые находки: - отсутствие baseline считалось доказательством, что Storage Sense отработал: '-not $LastRunBefore' истинно при неудачном чтении, и первая же проверка возвращала 'finished' для задачи, которая могла не стартовать -> пропуск всех 23 обработчиков. Теперь это отдельный исход 'unverifiable' - незапустившийся cleanmgr давал 15 минут выдуманного прогресса: Start-Process оставляет $null, а $null.HasExited тоже $null, поэтому '-not' истинно (измерено). Плюс ложный DiskCleanupPending в JSON - два fail-open в разборе ссылок: нечитаемый предок молча считался «не ссылкой», а исчерпание цикла возвращало частично разрешённый путь вместо $null - комментарий в той же защите путей утверждал обратное коду - тот же класс, что инцидент «fail closed» в bootstrap 2.17, попавший тогда в SECURITY.md - таймаут Storage Sense снова WARNING, «task stopped» только после проверки, неоднозначный поиск больше не сообщает заодно «задача не найдена», DriveNotFoundException считается отсутствием, а не невозможностью осмотра - -SkipDiskCleanup пропускал обещанную уборку StateFlags и получал зачисление освобождённых байтов на отключённый шаг - релиз-нота, вставленная в help функции Invoke-Phase, удовлетворяла проверку гейта самостоятельно: настоящую .RELEASENOTES можно было удалить и остаться зелёным. Проверка сужена до блока PSScriptInfo, инвариант закреплён тестом Документация: убрано недоказанное объяснение «время уходит на сканирование» (наш же замер его опровергает), задокументирован -Culture, релиз-ноты больше не обещают, что 901-секундный прогон стал быстрым, docs/what-is-cleaned.md перечисляет реальные условия отката на cleanmgr, сэмпл result-json дополнен двумя новыми полями, диагностика winget дополнена веткой WOW6432Node. 433 -> 450 тестов. Четыре мутации проверены и пойманы: возврат [int], возврат fail-open по baseline, снятие guard на cleanmgr, возврат частичного пути.
Повторное кросс-движковое ревью коммита 18808ca. Промпт прямо просил считать фиксы дефектными, пока не доказано обратное - и это снова оправдалось. 1. Мой фикс fail-open по baseline отдавал 'unverifiable' уже на десятой секунде. Медленно стартующая задача могла начаться ПОСЛЕ этого возврата, и cleanmgr запускался бы рядом с ней. Исправлено иначе, чем предложил ревьюер: окно ожидания используется целиком, а прямое наблюдение задачи в состоянии Running принимается как доказательство само по себе - ему baseline не нужен. 'unverifiable' отдаётся только если за всё окно задачу так и не увидели. 2. Обход предков уходил выше корня UNC-шары: Split-Path превращает \server\share в \server, Get-Item там не работает, и новое fail-closed правило отвергало бы ЛЮБОЙ UNC-путь очистки. Обход останавливается на корне тома. Проверено на живых путях: Temp, WINDOWS\Temp и ProgramData разрешаются как прежде и не отвергаются. 3. Сужение проверки релиз-ноты до блока PSScriptInfo оказалось недостаточным: строка 'vX.Y:' под любым другим полем всё ещё удовлетворяла её. Теперь читается именно секция .RELEASENOTES. 450 -> 452 теста. Отдельно, не дефект продукта: прогон стенда RU Full упал на доставке с 'Size mismatch after upload'. Причина операционная - я правил WinClean.ps1, пока стенд загружал его чанками. Харнесс отработал правильно: сверил размер и упал закрыто вместо заливки битого скрипта.
Description / Описание
WinClean v2.20 - a correctness and honesty round. No new cleanup features. It comes out of a full audit of the code base, a third-party review and an independent Codex pass, and it fixes one security-relevant gap, four operations that reported success while doing nothing, and a fast path that turned out to have been unreachable since it was written.
RU: раунд корректности и честности. Новых функций очистки нет. Источник - полный аудит кодовой базы, стороннее ревью и независимый проход Codex.
Type of Change / Тип изменения
Changes Made / Внесённые изменения
Security
GetFullPathdoes not resolve reparse points, so a link whose visible path looked harmless while pointing atProgram Filespassed the guard; enumerating that link then lists the target's contents. Verified on this machine before and after the fix: the same call returnedFalse(proceed) and now returnsTrue(refuse), and the junction really did exposeProgram Files. The scope was narrowed by measurement rather than assumed: links deeper in a tree were already safe, so only the root needed the change, and a link pointing somewhere harmless is still cleaned.Silent failures
npm cache cleannever had its exit code read, so a locked cache printed "npm cache cleaned".Remove-Item -ErrorAction SilentlyContinuecannot throw and the surrounding catch was dead code. Success is now confirmed by counting registry values before and after.Honest reporting
LoggingDegradedin the result JSON.DiskCleanupPendingin the JSON: the totals printed after it are partial. Its registry configuration is no longer swept while it is still running.The 15-minute run
\Microsoft\Windows\DiskCleanup\, where it does not exist - the real task is under\Microsoft\Windows\DiskFootprint\. Every run therefore fell back tocleanmgr: 901 seconds of an 1101-second run on a real workstation, and it did not finish. Fixing the path alone was not enough: a task that ran and failed was logged as "completed" and the fallback was skipped, so the result is now verified againstLastTaskResult.-SkipDiskCleanupskips only that step. Until now the only way to avoid the slowest step was-SkipCleanup, which suppresses every category.Release infrastructure
Install-Modulewould have split the two. Both now go throughtools/Invoke-Tests.ps1, the same script CI runs.Testing / Тестирование
Tested with
-ReportOnlyflagTested on Windows 11 with PowerShell 7.1+
Tested with various skip flags
Verified logging works correctly
394 Pester tests, none skipped (376 before). New coverage: the junction guard (a link to a protected root is refused, a harmless one is not), a fresh per-run statistics object, the registry value counter, and a mocked event-log enumeration failure.
Two logging tests could not fail: their only assertion sat inside
if (Test-Path $log), so a missing log file - the defect they exist to catch - made them pass. Both fixes were verified by mutation: breaking the product in the matching way now fails the test.pwsh tools/Invoke-ReleaseCheck.ps1: all substantive checks green.Checklist / Чеклист
Invoke-ReleaseCheck.ps1runAdditional Notes / Дополнительные заметки
Deliberately not changed: the "nothing freed, N still present" warning in
Remove-FolderContent. There the candidates really were selected and the deletion really did fail, so the warning is honest - unlike the Delivery Optimization one.Still open and tracked separately: measuring which
cleanmgrcategory actually consumes the time (it needs a destructive run on a real workstation), trimming the category list based on that measurement, and a locale-independent size parse.