tests/BugAudit.Tests.ps1
|
# Regression tests for the bugs found in the 011 exhaustive audit. Each Describe names the real # defect it locks down, so a future change that reintroduces it fails here instead of in a user's # terminal (or, for the uninstaller one, on a user's registry). $moduleRoot = Split-Path -Parent $PSScriptRoot Import-Module (Join-Path $moduleRoot 'Octa.psd1') -Force . (Join-Path $moduleRoot 'private\RunRecord.ps1') . (Join-Path $moduleRoot 'private\StateSnapshot.ps1') . (Join-Path $moduleRoot 'public\Invoke-OctaUninstaller.ps1') . (Join-Path $moduleRoot 'public\Invoke-OctaDiskCleanup.ps1') # Dot-sourced so Pester's Mock (which only sees the test scope, not the module's own session # state) can substitute them for the restore-point Describe below. . (Join-Path $moduleRoot 'private\ActionHelpers.ps1') . (Join-Path $moduleRoot 'private\ElevationCheck.ps1') . (Join-Path $moduleRoot 'private\StatusPanel.ps1') . (Join-Path $moduleRoot 'private\RestorePoint.ps1') . (Join-Path $moduleRoot 'private\Language.ps1') . (Join-Path $moduleRoot 'public\Get-OctaCategory.ps1') . (Join-Path $moduleRoot 'public\Invoke-OctaCategory.ps1') . (Join-Path $moduleRoot 'public\Invoke-OctaScheduler.ps1') # The dot-sourced Get-OctaCategory resolves each category by calling its metadata function by # name, so those have to exist in this scope too - without them it throws CommandNotFoundException # instead of returning the catalog. Get-ChildItem (Join-Path $moduleRoot 'categories\*.ps1') | ForEach-Object { . $_.FullName } Describe 'Uninstaller leftovers never target a still-installed vendor (011 audit)' { # The real defect: leftover detection matched on Publisher alone, so uninstalling one of the # 17 programs shipping as Publisher 'NVIDIA Corporation' on a real machine offered to delete # HKLM:\SOFTWARE\NVIDIA Corporation and %ProgramData%\NVIDIA Corporation - shared by the # graphics driver and the 16 other NVIDIA programs still installed. Publisher 'Microsoft' # would have offered HKLM:\SOFTWARE\Microsoft, i.e. Windows itself. It 'ignores a Publisher that another installed program still uses' { $installed = @(Get-OctaInstalledPrograms) $sharedPublisher = $installed | Group-Object Publisher | Where-Object { $_.Name -and $_.Count -gt 1 } | Select-Object -First 1 if (-not $sharedPublisher) { Set-ItResult -Skipped -Because 'no vendor ships more than one program on this machine' return } $markerPath = Get-OctaUninstallMarkerPath $backup = if (Test-Path $markerPath) { Get-Content $markerPath -Raw } else { $null } try { [pscustomobject]@{ DisplayName = 'Octa Audit Fake Program' Publisher = $sharedPublisher.Name InstallLocation = $null } | ConvertTo-Json | Set-Content -Path $markerPath -Encoding utf8 $output = Invoke-OctaUninstaller -CleanLeftovers 6>&1 | Out-String $output | Should Not Match ([regex]::Escape($sharedPublisher.Name)) } finally { if ($backup) { Set-Content -Path $markerPath -Value $backup -Encoding utf8 } elseif (Test-Path $markerPath) { Remove-Item $markerPath -Force -ErrorAction SilentlyContinue } } } } Describe 'Disk cleanup reports bytes actually freed, not bytes measured (011 audit)' { # The real defect: FreedBytes summed each target's pre-scan MeasuredBytes, while the deletion # ran with -ErrorAction SilentlyContinue. A locked file was skipped and still counted as # reclaimed - measured for real: a 1 MB open file survived and Octa reported 1 MB freed. It 'excludes files that survived the delete from the freed total' { $dir = Join-Path $env:TEMP "octa-audit-$([guid]::NewGuid().ToString('N'))" New-Item -ItemType Directory -Path $dir -Force | Out-Null $stream = $null try { [System.IO.File]::WriteAllBytes((Join-Path $dir 'gone.bin'), (New-Object byte[] 262144)) [System.IO.File]::WriteAllBytes((Join-Path $dir 'kept.bin'), (New-Object byte[] 262144)) $measured = Measure-OctaPathBytes -Paths @("$dir\*") $stream = [System.IO.File]::Open((Join-Path $dir 'kept.bin'), 'Open', 'Read', 'None') Remove-Item -Path "$dir\*" -Recurse -Force -ErrorAction SilentlyContinue $remaining = Measure-OctaPathBytes -Paths @("$dir\*") $freed = [Math]::Max(0L, [int64]$measured - [int64]$remaining) $measured | Should Be 524288 $remaining | Should Be 262144 $freed | Should Be 262144 } finally { if ($stream) { $stream.Dispose() } Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue } } } Describe 'Undo reports failure honestly (011 audit)' { # The real defect: the run was marked 'Undone' and Status 'Success' returned unconditionally, # even when every restore threw - which then made a retry answer 'AlreadyUndone', permanently # blocking an undo that never happened. $runsRoot = Get-OctaRunsRoot $runId = '20991231-235959' $runFolder = Join-Path $runsRoot $runId BeforeEach { New-Item -ItemType Directory -Path $runFolder -Force | Out-Null [pscustomobject]@{ RunId = $runId Timestamp = (Get-Date).ToString('o') CategoriesApplied = @('telemetry') ActionSnapshots = @( [pscustomobject]@{ ActionRef = 'OctaAuditMissingService' Type = 'Service' State = [pscustomobject]@{ Type = 'Service'; Name = 'OctaAuditNoSuchService'; PriorStartMode = 'Auto' } } ) IrreversibleActionRefs = @() Status = 'Completed' } | ConvertTo-Json -Depth 10 | Set-Content (Join-Path $runFolder 'run.json') -Encoding utf8 } AfterEach { Remove-Item $runFolder -Recurse -Force -ErrorAction SilentlyContinue } It 'returns Failed - not Success - when nothing could be restored' { $result = Undo-OctaRun -RunId $runId 6>$null $result.Status | Should Be 'Failed' @($result.Restored).Count | Should Be 0 } It 'still allows a retry instead of answering AlreadyUndone' { Undo-OctaRun -RunId $runId 6>$null | Out-Null $second = Undo-OctaRun -RunId $runId 6>$null $second.Status | Should Not Be 'AlreadyUndone' } } Describe 'Run folders are unique within the same second (011 audit)' { # The real defect: the id is second-resolution, so two runs started in the same second shared # one folder and the second run's run.json overwrote the first's - silently destroying the # first run's ability to be undone. Verified real: two back-to-back calls returned one id. It 'never hands out the same RunId twice' { $created = @() try { $created = @(1..3 | ForEach-Object { New-OctaRunFolder }) @($created.RunId | Select-Object -Unique).Count | Should Be 3 } finally { foreach ($c in $created) { Remove-Item $c.Path -Recurse -Force -ErrorAction SilentlyContinue } } } It 'keeps ids sortable so Get-OctaRunRecord -RunId latest still resolves newest-last' { $created = @() try { $created = @(1..3 | ForEach-Object { New-OctaRunFolder }) $sorted = @($created.RunId | Sort-Object) $sorted[-1] | Should Be $created[-1].RunId } finally { foreach ($c in $created) { Remove-Item $c.Path -Recurse -Force -ErrorAction SilentlyContinue } } } } Describe 'History survives an unreadable run record (011 audit)' { # The real defect: one malformed run.json threw out of ConvertFrom-Json and took the entire # history listing with it. It 'skips the bad record instead of throwing' { $badRun = Join-Path (Get-OctaRunsRoot) '20990101-010101' New-Item -ItemType Directory -Path $badRun -Force | Out-Null Set-Content -Path (Join-Path $badRun 'run.json') -Value '{ not valid json' -Encoding utf8 try { { Show-OctaHistory 6>$null } | Should Not Throw } finally { Remove-Item $badRun -Recurse -Force -ErrorAction SilentlyContinue } } } Describe 'Read-only tools fail loudly, not silently (011 audit)' { It 'Show-OctaDiskUsage reports a missing path instead of an empty result' { $result = Show-OctaDiskUsage -Path 'C:\OctaAuditPathThatDoesNotExist' 6>$null $result.Status | Should Be 'PathNotFound' } It 'Invoke-OctaScheduler refuses an unknown category before registering a task' { $result = Invoke-OctaScheduler -Create -Category 'octa-audit-not-a-category' -Frequency 'Daily' 6>$null $result.Status | Should Be 'UnknownCategory' } It 'Invoke-OctaTaskBrowser reports NotFound for a task that does not exist' { $result = Invoke-OctaTaskBrowser -Disable '\OctaAudit\NoSuchTask' 6>$null $result.Status | Should Be 'NotFound' } } Describe 'Wi-Fi profile names keep their colons (011 audit)' { # The real defect: the regex was greedy, so it split on the LAST colon - a profile named # "Red: Casa" came back as "Casa", which then failed its own -notin $known lookup. It 'splits on the first colon, matching the documented behaviour' { $line = ' All User Profile : Red: Casa' $line -match '^\s+.+?:\s*(\S.*)$' | Should Be $true $Matches[1].Trim() | Should Be 'Red: Casa' } } Describe 'Restore-point warning is shown once, not twice (011 audit)' { # The real defect, captured verbatim in a terminal screenshot: declining "Enable System # Protection?" printed the identical "System Protection is off" warning a second time and # asked a second confirm prompt, because the offer-to-enable block and the proceed-anyway # block both emitted the same string for the same status. # # Fully synthetic category so this touches nothing real on the machine running the tests - # no registry write, no service change, no restore point. function Get-OctaAuditFakeActions { return @(New-OctaAction -TargetType Registry -TargetIdentifier 'HKCU:\Software\OctaAudit|FakeValue' ` -CurrentValue '(not set)' -PlannedValue 1 -Reversible $true) } function Set-OctaAuditFakeAction { param($Action) } It 'prints the warning exactly once and asks to proceed exactly once' { Mock Get-OctaCategory { [pscustomobject]@{ Id = 'octa-audit-fake' DisplayName = 'Octa Audit Fake' Description = 'synthetic' RequiresElevation = $false ContainsIrreversibleActions = $false GetActionsFunction = 'Get-OctaAuditFakeActions' ApplyActionFunction = 'Set-OctaAuditFakeAction' HighestRiskLevel = 'Safe' } } Mock New-OctaRestorePoint { [pscustomobject]@{ Status = 'SkippedProtectionDisabled'; SequenceNumber = $null } } Mock Save-OctaActionSnapshot { $null } Mock Save-OctaRunRecord { } Mock New-OctaRunFolder { [pscustomobject]@{ RunId = 'audit'; Path = $env:TEMP } } Mock Read-Host { if ($Prompt -match 'Enable System Protection') { return 'n' } return 's' } $strings = Get-OctaStrings $warning = $strings.restorePointSkippedProtectionDisabled $output = Invoke-OctaCategory -CategoryId 'octa-audit-fake' -Apply 6>&1 | Out-String $warningCount = ([regex]::Matches($output, [regex]::Escape($warning))).Count $warningCount | Should Be 1 Assert-MockCalled Read-Host -Times 1 -Exactly -ParameterFilter { $Prompt -match 'Enable System Protection' } } } Describe 'Recycle Bin measurement does not overflow for large files (011 audit)' { # The real defect, found while re-verifying the earlier "flaky" DiskHealth non-mutation fix # on this same machine: Shell.Application's Namespace(10).Items().Size is a signed Int32, # which overflows negative for any single Recycle Bin item over 2 GB. Measured for real: a # Recycle Bin holding a ~4 GB Windows ISO and several 1+ GB files summed to # -1,052,385,010 bytes via the COM property, and 25,916,231,884 (the correct figure) via # direct filesystem measurement - which then fed into the overall "Total reclaimable" GB # figure Octa shows the user. It 'never reports negative bytes for any cleanup target' { $targets = @(Get-OctaCleanupTargets) $negative = @($targets | Where-Object { $_.MeasuredBytes -lt 0 }) $negative.Count | Should Be 0 } It 'measures the Recycle Bin the same way as a direct filesystem scan' { $viaOcta = Measure-OctaRecycleBinBytes $direct = 0L foreach ($drive in @(Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3')) { $p = Join-Path $drive.DeviceID '$Recycle.Bin' if (Test-Path -LiteralPath $p) { $direct += (Get-ChildItem -LiteralPath $p -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum } } $viaOcta | Should Be $direct } } Describe 'Scheduler reports a failed registration as failed (011 audit)' { # The real defect behind the "flaky" Monthly test: Register-ScheduledTask only raises a # NON-terminating error, so execution fell straight through to the Success return. A Monthly # task that never registered was reported as created, with a TaskName that could not then be # found or removed. It 'returns Error, not Success, when registration fails' { Mock Register-ScheduledTask { throw 'simulated registration failure' } $result = Invoke-OctaScheduler -Create -Category 'telemetry' -Frequency 'Daily' 6>$null $result.Status | Should Be 'Error' } } Describe 'Scheduler Monthly trigger genuinely registers (011 audit)' { # Not a flake: MSFT_TaskMonthlyTrigger built via CIM was rejected by Register-ScheduledTask # with E_INVALIDARG on every variant tried (plain assignment, explicit [uint16] casts, # RunOnLastDayOfMonth, timezone-qualified StartBoundary, New-ScheduledTask + -InputObject). # It failed 100% of the time. Registering the schedule as task XML works. It 'creates a real day-1-of-every-month task that can be listed and removed' { if (-not (Test-OctaElevation)) { Set-ItResult -Skipped -Because 'registering a scheduled task requires elevation' return } $created = Invoke-OctaScheduler -Create -Category 'telemetry' -Frequency 'Monthly' 6>$null try { $created.Status | Should Be 'Success' $found = Get-ScheduledTask -TaskPath '\Octa\' -TaskName $created.TaskName -ErrorAction SilentlyContinue $found | Should Not Be $null $doc = [xml](Export-ScheduledTask -TaskPath '\Octa\' -TaskName $created.TaskName) $ns = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) $ns.AddNamespace('t', 'http://schemas.microsoft.com/windows/2004/02/mit/task') # A genuine calendar-monthly schedule, not an "every 4 weeks" approximation. @($doc.SelectNodes('//t:ScheduleByMonth/t:DaysOfMonth/t:Day', $ns)).Count | Should Be 1 @($doc.SelectNodes('//t:ScheduleByMonth/t:Months/*', $ns)).Count | Should Be 12 } finally { if ($created.TaskName) { Invoke-OctaScheduler -Remove $created.TaskName 6>$null | Out-Null } } } } Describe 'Run records describe the restore point honestly (011 audit)' { # Tools that never attempt a restore point recorded 'SkippedByUser', which describes a # decision the user was never asked to make. It 'never claims the user skipped a restore point that was never offered' { $offenders = @( Get-ChildItem (Join-Path $moduleRoot 'public\*.ps1') | Where-Object { (Get-Content $_.FullName -Raw) -match "RestorePointStatus\s*=\s*'SkippedByUser'" } ) $offenders.Count | Should Be 0 } } Describe 'ExplorerStart never offers an HKLM-only action without elevation (011 audit, second pass)' { # Real defect: "Include in library" and "Give access to" context-menu-handler keys exist # ONLY under HKLM\SOFTWARE\Classes on a real machine, never under HKCU\Software\Classes. # Measured directly: BUILTIN\Usuarios (the standard Users group) has ReadKey only on both, # no delete/write right at all - yet the whole category is RequiresElevation = $false, # because most of its OTHER actions genuinely are per-user HKCU values. Without gating these # two specifically, a non-elevated run offered to remove them, the delete then failed # silently (-ErrorAction SilentlyContinue), and the category still reported Success. It 'drops HKCR-only actions when not elevated, keeps genuine HKCU ones' { Mock Test-OctaElevation { return $false } $actions = @(Get-OctaExplorerStartActions) $hklmOnly = @($actions | Where-Object { $_.TargetIdentifier -like 'HKCR:*' }) foreach ($a in $hklmOnly) { $userOverride = $a.TargetIdentifier -replace '^HKCR:', 'HKCU:\Software\Classes' $userOverride = ($userOverride -split '\|', 2)[0] (Test-Path -LiteralPath $userOverride) | Should Be $true } } It 'still offers every action when elevated' { # Real bug found running this suite on a dev machine that had already applied # explorer-start once (via Quick Clean): every HKCU value already matched its planned # value, so Get-OctaExplorerStartActions legitimately returned 0 actions in both modes, # and 'Should BeGreaterThan 0' failed even though the code under test was correct - the # test was reading live, mutable machine state instead of controlling it. Force one # HKCU value away from its target here (save/restore, same try/finally pattern as the # other fixtures in this file) so the assertion is deterministic regardless of whether # explorer-start has already been run on the machine executing the suite. $regPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' $priorHideFileExt = (Get-ItemProperty -Path $regPath -Name 'HideFileExt' -ErrorAction SilentlyContinue).HideFileExt try { Set-ItemProperty -Path $regPath -Name 'HideFileExt' -Value 1 -Type DWord Mock Test-OctaElevation { return $true } $elevated = @(Get-OctaExplorerStartActions) Mock Test-OctaElevation { return $false } $nonElevated = @(Get-OctaExplorerStartActions) $nonElevated.Count | Should BeLessThan ($elevated.Count + 1) $elevated.Count | Should BeGreaterThan 0 } finally { if ($null -eq $priorHideFileExt) { Remove-ItemProperty -Path $regPath -Name 'HideFileExt' -ErrorAction SilentlyContinue } else { Set-ItemProperty -Path $regPath -Name 'HideFileExt' -Value $priorHideFileExt -Type DWord } } } } Describe 'OemSuites AURA pattern does not match unrelated words (011 audit, second pass)' { # Real defect: bare "AURA" is an unanchored, case-insensitive substring match, so it matched # inside the Spanish word "restauracion" (as in "restore"). Verified on a real Spanish- # language Windows install: Microsoft's own "Cloud backup and restore" service - # "Servicio de copia de seguridad y restauracion en la nube" - got hardcoded-pattern-matched # as ASUS bloat, through the branch with NO separate confirmation gate (unlike a # HeuristicMatch hit). It 'does not match a Spanish word that merely contains the substring AURA' { $pattern = ($script:OctaOemHardcodedPatterns | Where-Object { $_.Vendor -eq 'ASUS' }).Pattern 'Servicio de copia de seguridad y restauracion en la nube' -match $pattern | Should Be $false } It 'still matches the real ASUS AURA Sync lighting service' { $pattern = ($script:OctaOemHardcodedPatterns | Where-Object { $_.Vendor -eq 'ASUS' }).Pattern 'ASUS AURA SYNC lighting service' -match $pattern | Should Be $true } } Describe 'Services category scan completes quickly (011 audit, second pass)' { # Real defect, found while re-running the full suite after the ExplorerStart/OemSuites # fixes above (unrelated to them - the code had already been like this): one -Filter # "Name='X'" CIM query per catalog entry (35 of them) took ~10.1s total on a real machine, # right at Categories.Tests.ps1's own 10-second "completes quickly" threshold - measured # ~290ms of WQL overhead per filtered call, vs 0.47s to fetch every service in one # unfiltered call. Same root cause as the earlier Get-OctaCategory startup-delay fix: many # small expensive calls instead of one cheap bulk one. It 'scans the whole 35-entry catalog in well under a second' { $sw = [System.Diagnostics.Stopwatch]::StartNew() Get-OctaServicesActions | Out-Null $sw.Stop() $sw.Elapsed.TotalSeconds | Should BeLessThan 3 } } Describe 'Category apply functions use the action PlannedValue (011 audit)' { # Copilot/Recall hardcoded "-Value 1" instead of reading PlannedValue, so a future change to # the planned value would be silently ignored at apply time while the preview still showed it. It 'has no category writing a hardcoded DWord instead of PlannedValue' { $offenders = @( Get-ChildItem (Join-Path $moduleRoot 'categories\*.ps1') | Where-Object { (Get-Content $_.FullName -Raw) -match 'Set-ItemProperty[^\r\n]*-Value \d+ -Type DWord' } ) $offenders.Count | Should Be 0 } } |