diff --git a/CHANGELOG.md b/CHANGELOG.md index 860425e..1b65c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **`install.ps1` refused to run on every released PowerShell 7, saying it could not read + the version.** Reported from a user machine running 7.6.4. `pwsh.exe` reports + `ProductVersion` as `7.6.4 SHA: +` - the commit hash follows a **space**, + while the code stripped a `-suffix` (which only covers preview builds), so the `[version]` + cast threw and the version came out `$null`. The numeric `ProductMajorPart` / + `ProductMinorPart` / `ProductBuildPart` fields are read instead; there is no string to + misparse. The parse had been broken since the check was written - the pre-2.22 form + `if ($pwshVersion -and ...)` skipped the comparison instead of reporting it, so the + installer silently ran with **no version check at all** for three releases. Making the + check fail-closed in 2.22 is what made it visible. This fix is live as soon as it reaches + `main`: the one-liner fetches `install.ps1` from the branch, not from a release asset, so + no new release is required. `get.ps1` was never affected - it runs inside PowerShell and + reads `$PSVersionTable`. +- The only guard on that check was a **grep test**: it asserted the shape of the code and + never evaluated it, which is why 702 green tests did not notice. It is now joined by a + behavioural test that executes `install.ps1`'s own expression against the real `pwsh.exe` + on the test machine and asserts the result clears 7.1. Verified by mutation: with the old + expression restored, the behavioural test fails while the grep test beside it still + passes. 702 -> 704 tests. + Planned for a later release: quick system health section (SMART, image integrity, WinRE), Windows Update driver listing, run-to-run delta and HTML report. See CLAUDE.md. diff --git a/CLAUDE.md b/CLAUDE.md index 620480c..2f2b573 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,9 +43,9 @@ CleanScript/ │ └── logo.svg # Логотип проекта ├── get.ps1 # Bootstrap: разовый запуск одной командой (irm | iex) ├── install.ps1 # Bootstrap: установка/обновление + ярлык (RunAs admin) -├── tests/ # Pester тесты (702 всего; счётчик - прогоном, не грепом) +├── tests/ # Pester тесты (704 всего; счётчик - прогоном, не грепом) │ ├── Helpers.Tests.ps1 # Unit-тесты helper-функций (378, дот-сорсят продукт - нужны права админа) -│ ├── Fixes.Tests.ps1 # Валидационные тесты исправлений (206) +│ ├── Fixes.Tests.ps1 # Валидационные тесты исправлений (208) │ ├── Integration.Tests.ps1 # Интеграционные тесты в песочнице ФС (67, требуют admin) │ ├── StandHelpers.Tests.ps1 # Чистые хелперы стенда: dead-man + версионный гейт + таблица истинности report-гварда (23, без admin) │ └── Docs.Tests.ps1 # Гварды документации: нет тире + нет битых ссылок (28, без admin; файлы ПЕРЕЧИСЛЯЮТСЯ, а не задаются списком) @@ -198,7 +198,7 @@ Publish-PSResource -Path .\WinClean.ps1 -Repository PSGallery -ApiKey $env:PSGAL **Проверки (4 job'а, счёт сверять gh run view --json jobs, а не этой строкой):** 1. **lint** - PSScriptAnalyzer (Error+Warning) через общий ools/Invoke-Lint.ps1 - тот же, что зовёт релиз-гейт 2. **syntax** - Проверка синтаксиса PowerShell -3. **test** - Pester тесты (702; интеграционные требуют admin - на GitHub runners это выполняется) +3. **test** - Pester тесты (704; интеграционные требуют admin - на GitHub runners это выполняется) 4. **smoke** - прогон -ReportOnly + геометрия рамок + result JSON **Исключения PSScriptAnalyzer** (допустимые для CLI): @@ -208,7 +208,7 @@ Publish-PSResource -Path .\WinClean.ps1 -Repository PSGallery -ApiKey $env:PSGAL ### Pester тесты (v2.13+) -- `tests/Helpers.Tests.ps1` - 378 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 206, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 23 (без admin), `tests/Docs.Tests.ps1` - 28 (гварды доков, без admin) +- `tests/Helpers.Tests.ps1` - 378 unit-тестов (дот-сорсят WinClean.ps1), `tests/Fixes.Tests.ps1` - 208, `tests/Integration.Tests.ps1` - 67 (песочница ФС, требуют admin), `tests/StandHelpers.Tests.ps1` - 23 (без admin), `tests/Docs.Tests.ps1` - 28 (гварды доков, без admin) - Особенности: функции в BeforeAll (не AST), regex для locale-независимости, отдельные It блоки --- @@ -447,7 +447,7 @@ pwsh tools/Invoke-ReleaseCheck.ps1 # версия во всех pwsh tools/Invoke-ReleaseCheck.ps1 -IncludeStand # + боевой прогон на VM (минуты) pwsh tools/Invoke-ReleaseCheck.ps1 -VerifyPublished # ПОСЛЕ выпуска: ассеты релиза и SHA256 -Invoke-Pester ./tests -Output Detailed # 702 Pester тестов (считать прогоном, не грепом) +Invoke-Pester ./tests -Output Detailed # 704 Pester тестов (считать прогоном, не грепом) pwsh tools/Invoke-SmokeTest.ps1 # Смоук: ReportOnly + геометрия UI pwsh tools/proxmox/Invoke-StandTest.ps1 -Mode Report # Стенд на Proxmox (RU=VM 190, EN: -ConfigPath ...en.json = VM 191) # Ночная матрица: cron 03:30 на proxmos (/opt/winclean-stand, /etc/cron.d/winclean-stand), отчёт в Telegram diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c40ffc..ea13bf0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -128,7 +128,7 @@ Invoke-Pester ./tests/Integration.Tests.ps1 All PRs automatically run: - PSScriptAnalyzer (linting) - Syntax check -- Pester tests (702 tests) +- Pester tests (704 tests) ### Release-impacting changes @@ -284,7 +284,7 @@ Invoke-Pester ./tests/Integration.Tests.ps1 Все PR автоматически проходят: - PSScriptAnalyzer (линтинг) - Проверка синтаксиса -- Pester тесты (702 тестов) +- Pester тесты (704 тестов) ### Изменения, влияющие на релиз diff --git a/install.ps1 b/install.ps1 index 69c89f8..b4b803a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -84,7 +84,23 @@ if (-not (Test-Path $pwshPath)) { # had established. That is the same fail-open shape as the SHA256 verification that hid # inside `if ($hashAsset)` until v2.17: a check that cannot run becomes a check that # passes. Absence of evidence is not evidence of compatibility. -$pwshVersion = try { [version](((Get-Item $pwshPath).VersionInfo.ProductVersion -split '-')[0]) } catch { $null } +# +# Read the NUMERIC version fields rather than parsing the display string. pwsh.exe reports +# ProductVersion as "7.6.4 SHA: 929d27f4...+929d27f4..." - the commit hash is appended after +# a SPACE, so stripping a "-suffix" (which only covers preview builds like "7.7.0-preview.2") +# left the hash in place and the [version] cast threw on every released PowerShell 7. Making +# the check fail-closed above is what surfaced it: the parse had never worked, and the old +# form skipped the comparison instead of reporting it, so the failure was invisible for three +# releases. ProductMajorPart/MinorPart/BuildPart are integers taken straight from the version +# resource - there is no string to misparse. +$pwshVersion = try { + $vi = (Get-Item $pwshPath).VersionInfo + # A genuine PowerShell 7 binary never reports major 0; that value means the version + # resource is missing or unreadable, which must stay fail-closed. + if ($vi.ProductMajorPart -gt 0) { + [version]::new($vi.ProductMajorPart, $vi.ProductMinorPart, $vi.ProductBuildPart) + } else { $null } +} catch { $null } if (-not $pwshVersion) { # Deliberately a different message from "your version is too old": the user needs to # know the file is there but unreadable, which points at a damaged or substituted diff --git a/tests/Fixes.Tests.ps1 b/tests/Fixes.Tests.ps1 index c50e901..2b976f7 100644 --- a/tests/Fixes.Tests.ps1 +++ b/tests/Fixes.Tests.ps1 @@ -925,6 +925,46 @@ Describe "v2.17: bootstrap verification is mandatory" -Tag "Fix", "V217" { $code | Should -Match 'version could not be read' $code | Should -Not -Match '\$pwshVersion -and \$pwshVersion -lt' } + + It "install.ps1 actually reads the version of the real pwsh.exe (v2.23)" { + # Reported from a user machine running PowerShell 7.6.4: the installer refused to + # run, saying the version could not be read. It could not - pwsh reports + # ProductVersion as "7.6.4 SHA: +", the hash separated by a SPACE, so + # stripping a "-suffix" left it in place and the [version] cast threw on EVERY + # released PowerShell 7. The parse had been broken since it was written; the + # fail-open form above it skipped the comparison instead of reporting it, which is + # exactly why nobody could see it for three releases. + # + # The grep test above passed throughout, because it checks the SHAPE of the code and + # never evaluates it. This one runs install.ps1's own expression against the real + # binary on this machine - the only way the defect could have been caught. + $installPath = (Resolve-Path (Join-Path $PSScriptRoot '..' 'install.ps1')).Path + $ast = [System.Management.Automation.Language.Parser]::ParseFile($installPath, [ref]$null, [ref]$null) + $assign = @($ast.FindAll({ + param($n) + $n -is [System.Management.Automation.Language.AssignmentStatementAst] -and + $n.Left.Extent.Text -eq '$pwshVersion' + }, $true))[0] + $assign | Should -Not -BeNullOrEmpty -Because 'install.ps1 must still compute $pwshVersion' + + $pwshPath = Join-Path ([Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles)) 'PowerShell\7\pwsh.exe' + $pwshPath | Should -Exist -Because 'these tests run under PowerShell 7 from the canonical location' + + # Run the product's line, then hand its result back out of the scriptblock. + $result = & ([scriptblock]::Create($assign.Extent.Text + "`n`$pwshVersion")) + + $result | Should -Not -BeNullOrEmpty -Because 'a working PowerShell 7 must yield a version' + $result | Should -BeOfType ([version]) + ($result -ge [version]'7.1') | Should -BeTrue -Because "the parsed version ($result) must clear the 7.1 gate" + } + + It "install.ps1 does not parse the pwsh version out of the display string (v2.23)" { + # ProductVersion is a display string that carries a commit hash; the numeric + # ProductMajorPart/MinorPart/BuildPart fields cannot be misparsed. + $code = ($installScript -split "`n" | Where-Object { $_ -notmatch '^\s*#' }) -join "`n" + $code | Should -Not -Match 'ProductVersion\s+-split' + $code | Should -Match 'ProductMajorPart' + } } Describe "v2.18: bootstrap host allowlist is exact, not a broad suffix" -Tag "Fix", "V218" -ForEach @(