tests/Categories.Tests.ps1

$moduleRoot = Split-Path -Parent $PSScriptRoot
Import-Module (Join-Path $moduleRoot 'Octa.psd1') -Force

# Category and helper functions are internal (not in FunctionsToExport), so dot-source them
# directly for white-box testing - mirrors what Octa.psm1 does on import.
. (Join-Path $moduleRoot 'private\ActionHelpers.ps1')
. (Join-Path $moduleRoot 'private\ElevationCheck.ps1')
Get-ChildItem -Path (Join-Path $moduleRoot 'categories') -Filter '*.ps1' | ForEach-Object { . $_.FullName }

Describe 'New-OctaAction' {
    It 'throws when Reversible is $false without an IrreversibleReason' {
        { New-OctaAction -TargetType Service -TargetIdentifier 'Foo' -Reversible $false } | Should Throw
    }
    It 'accepts Reversible $false with a reason' {
        $a = New-OctaAction -TargetType Service -TargetIdentifier 'Foo' -Reversible $false -IrreversibleReason 'test'
        $a.IrreversibleReason | Should Be 'test'
    }
}

$categoryFunctions = @(
    'Get-OctaTelemetryCategory', 'Get-OctaAppxCategory', 'Get-OctaOneDriveCategory', 'Get-OctaCopilotCategory',
    'Get-OctaWidgetsCategory', 'Get-OctaRecallCategory', 'Get-OctaExplorerStartCategory', 'Get-OctaOemSuitesCategory',
    'Get-OctaPerformanceCategory', 'Get-OctaPowerCategory', 'Get-OctaUserExperienceCategory', 'Get-OctaGpuCategory',
    'Get-OctaServicesCategory', 'Get-OctaDesktopCategory', 'Get-OctaPreferencesCategory', 'Get-OctaGamingCategory',
    'Get-OctaAiFeaturesCategory', 'Get-OctaWindowsFeaturesCategory', 'Get-OctaBackgroundActivityCategory',
    'Get-OctaRegistryOrphansCategory'
)

$isElevated = Test-OctaElevation

foreach ($fn in $categoryFunctions) {
    Describe "$fn" {
        $category = & $fn
        # Some categories' scans genuinely require elevation to read all-user state (e.g.
        # Get-AppxPackage -AllUsers) - only exercise the scan itself when this test session
        # can actually satisfy that, same as Invoke-OctaCategory would gate it in real use.
        $canScan = $isElevated -or -not $category.RequiresElevation

        It 'has a non-empty Id and DisplayName' {
            $category.Id | Should Not BeNullOrEmpty
            $category.DisplayName | Should Not BeNullOrEmpty
        }

        It 'GetActionsFunction is a dry-run: it only reads state and never throws' {
            if (-not $canScan) { return }
            { & $category.GetActionsFunction } | Should Not Throw
        }

        It 'GetActionsFunction completes quickly (catches accidental registry wildcard hangs, e.g. HKCR:\*\...)' {
            if (-not $canScan) { return }
            $sw = [System.Diagnostics.Stopwatch]::StartNew()
            & $category.GetActionsFunction | Out-Null
            $sw.Stop()
            $sw.Elapsed.TotalSeconds | Should BeLessThan 10
        }

        It 'every returned action has the required fields, and irreversible ones carry a reason' {
            if (-not $canScan) { return }
            $actions = & $category.GetActionsFunction
            foreach ($a in $actions) {
                $a.TargetType | Should Not BeNullOrEmpty
                $a.TargetIdentifier | Should Not BeNullOrEmpty
                if (-not $a.Reversible) {
                    $a.IrreversibleReason | Should Not BeNullOrEmpty
                }
            }
        }
    }
}