tests/Install-DFTool.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/TestSupport.ps1"
    foreach ($f in Get-DFTestModuleFile) { . $f }
    function script:New-InstallTools {
        $dir = Join-Path $TestDrive "it-$([guid]::NewGuid().ToString('N').Substring(0,6))"
        New-Item -ItemType Directory $dir | Out-Null
        @{
            scoop = '{ "name": "scoop", "executable": "scoop.cmd", "roles": { "package-manager": { "priority": 30 } }, "installs": { "from": "scoop", "command": ["scoop","install","{id}"], "batch": true } }'
            npm   = '{ "name": "npm", "executable": "npm.cmd", "roles": { "js-package-manager": { "priority": 30 } }, "packages": { "scoop": "nodejs" }, "installs": { "from": "npm", "command": ["npm","i","-g","{id}"], "batch": true } }'
            pnpm  = '{ "name": "pnpm", "executable": "pnpm.cmd", "roles": { "js-package-manager": { "priority": 20 } }, "packages": { "scoop": "pnpm" }, "installs": { "from": "npm", "command": ["pnpm","add","-g","{id}"], "batch": true } }'
            glow  = '{ "name": "glow", "executable": "glow.exe", "packages": { "scoop": "glow" } }'
            ish   = '{ "name": "ish", "executable": "is.cmd", "packages": { "npm": "@microsoft/inshellisense" } }'
        }.GetEnumerator() | ForEach-Object { Set-Content (Join-Path $dir "$($_.Key).json") $_.Value }
        $dir
    }
}

Describe 'Install-DFTool' {
    BeforeEach {
        Set-DFTestXdg; Reset-DFTestSession; Set-DFTestConfig $null
        $script:Tools = New-InstallTools
        $script:Installed = @('scoop.cmd')
        $script:Calls = [System.Collections.Generic.List[string]]::new()
        Mock Test-DFToolAvailable { $Executable -in $script:Installed }
        Mock Invoke-DFInstallCommand {
            $script:Calls.Add($Argv -join ' ')
            # An install makes its tools "installed".
            $exe = @{ glow = 'glow.exe'; '@microsoft/inshellisense' = 'is.cmd'; nodejs = 'npm.cmd'; pnpm = 'pnpm.cmd' }
            if ($Argv[1] -in 'install', 'i', 'add') { $script:Installed += @($Argv | Select-Object -Skip 2 | ForEach-Object { $exe[$_] } | Where-Object { $_ }) }
            [pscustomobject]@{ ExitCode = 0; Output = '' }
        }
        Mock Update-DFPathFromRegistry { }
        Mock Test-DFElevated { $false }
        Mock Write-DFConflictNotice { }
        Mock Test-DFInteractiveHost { $false }
        Mock Read-DFInstallChoice { $Default }
    }
    AfterEach { Restore-DFTestXdg; Set-DFTestConfig $null }

    It '-Missing installs the session''s missing tools and activates them' {
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        $r = Install-DFTool -Missing -UseDefaults -ToolsPath $script:Tools 6>$null
        $script:Calls | Should -Be @('scoop install glow')
        ($r | Where-Object Tool -eq glow).Result | Should -Be 'Installed'
        (Get-DFToolStatus -Name glow).State | Should -Be 'Active'
    }
    It '-WhatIf prints the plan without PowerShell''s per-property What-if noise' {
        $out = Install-DFTool -Name glow -WhatIf -ToolsPath $script:Tools *>&1 | Out-String
        $out | Should -Not -Match 'Retrieve the value'
        $out | Should -Match 'scoop: glow'
    }
    It '-WhatIf installs nothing' {
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        $null = Install-DFTool -Missing -WhatIf -ToolsPath $script:Tools 6>$null
        $script:Calls | Should -BeNullOrEmpty
    }
    It 'a non-interactive host without -UseDefaults reports a gap and its dependents, and asks nothing' {
        $r = Install-DFTool -Name ish -ToolsPath $script:Tools 3>$null 6>$null
        Should -Invoke Read-DFInstallChoice -Times 0
        $script:Calls | Should -BeNullOrEmpty
        ($r | Where-Object Tool -eq ish).Result | Should -Be 'Gap'
        ($r | Where-Object Tool -eq ish).Detail | Should -Match 'npm, pnpm'
    }
    It '-UseDefaults fills a gap with the role''s top member and installs it first' {
        $null = Install-DFTool -Name ish -UseDefaults -ToolsPath $script:Tools 3>$null 6>$null
        $script:Calls | Should -Be @('scoop install nodejs', 'npm i -g @microsoft/inshellisense')
    }
    It 'interactive mode asks for each open choice, showing the default, and uses the answer' {
        Mock Test-DFInteractiveHost { $true }
        # Pick pnpm for the manager question; say yes to the plan.
        Mock Read-DFInstallChoice { if ($Options -contains 'pnpm') { 'pnpm' } else { 'y' } }
        $null = Install-DFTool -Name ish -ToolsPath $script:Tools 3>$null 6>$null
        Should -Invoke Read-DFInstallChoice -Times 1 -ParameterFilter { $Default -eq 'npm' -and $Options -contains 'pnpm' }
        Should -Invoke Read-DFInstallChoice -Times 1 -ParameterFilter { $Options -contains 'y' }
        $script:Calls | Should -Be @('scoop install pnpm', 'pnpm add -g @microsoft/inshellisense')
    }
    It 'interactive mode skips its confirmation with -Confirm:$false' {
        Mock Test-DFInteractiveHost { $true }
        Mock Read-DFInstallChoice { if ($Options -contains 'pnpm') { 'npm' } else { 'n' } }
        $null = Install-DFTool -Name ish -Confirm:$false -ToolsPath $script:Tools 3>$null 6>$null
        Should -Invoke Read-DFInstallChoice -Times 0 -ParameterFilter { $Options -contains 'y' }
        $script:Calls | Should -Be @('scoop install nodejs', 'npm i -g @microsoft/inshellisense')
    }
    It 'warns about InstallVia once, and checks each tool once, however many questions it asks' {
        Set-DFTestConfig @{ InstallVia = @{ ish = 'crates' } }
        Mock Test-DFInteractiveHost { $true }
        Mock Read-DFInstallChoice { if ($Options -contains 'pnpm') { 'npm' } else { 'n' } }
        $null = Install-DFTool -Name ish -ToolsPath $script:Tools -WarningVariable w 3>$null 6>$null
        @($w | Where-Object { "$_" -match 'InstallVia' }).Count | Should -Be 1
        Should -Invoke Test-DFToolAvailable -Times 1 -Exactly -ParameterFilter { $Executable -eq 'pnpm.cmd' }
    }
    It 'warns when InstallVia names a source whose manager isn''t installed, then uses the next source' {
        $glow = Join-Path $script:Tools 'glow.json'
        '{ "name": "glow", "executable": "glow.exe", "packages": { "scoop": "glow", "winget": "charm.glow" } }' | Set-Content $glow
        '{ "name": "winget", "executable": "winget.exe", "roles": { "package-manager": { "priority": 20 } }, "installs": { "from": "winget", "command": ["winget","install","{id}"] } }' |
            Set-Content (Join-Path $script:Tools 'winget.json')
        Set-DFTestConfig @{ InstallVia = @{ glow = 'winget' } }
        $null = Install-DFTool -Name glow -UseDefaults -ToolsPath $script:Tools -WarningVariable w 3>$null 6>$null
        "$w" | Should -Match "InstallVia.*glow.*winget.*not installed"
        $script:Calls | Should -Be @('scoop install glow')
    }
    It 'warns that a tool installed by name but not in Tools loads only this session' {
        Start-DFSession -Config @{ Tools = @() } -ToolsPath $script:Tools 3>$null
        $null = Install-DFTool -Name glow -UseDefaults -ToolsPath $script:Tools -WarningVariable w 3>$null 6>$null
        "$w" | Should -Match 'glow.*add it to Tools'
    }
    It 'does not reinstall what a previous partial run already installed' {
        $script:Installed += 'glow.exe'
        $null = Install-DFTool -Name glow -UseDefaults -ToolsPath $script:Tools 3>$null 6>$null
        $script:Calls | Should -BeNullOrEmpty
    }
    It '-Via installs from the named source for this call' {
        $null = Install-DFTool -Name glow -Via scoop -UseDefaults -ToolsPath $script:Tools 3>$null 6>$null
        $script:Calls | Should -Be @('scoop install glow')
    }
}

Describe 'Start-DFSession install hints' {
    BeforeEach { Set-DFTestXdg; Reset-DFTestSession; Set-DFTestConfig $null; $script:Tools = New-InstallTools; Mock Write-DFConflictNotice { } }
    AfterEach { Restore-DFTestXdg; Set-DFTestConfig $null }
    It 'reads no manager record when nothing is missing' {
        Mock Test-DFToolAvailable { $true }
        Mock New-DFInstallPlan { }
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        Should -Invoke New-DFInstallPlan -Times 0
    }
    It 'builds the hint only when the status is read, not at startup' {
        Mock Test-DFToolAvailable { $Executable -eq 'scoop.cmd' }
        Mock New-DFInstallPlan { }
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        Should -Invoke New-DFInstallPlan -Times 0
    }
    It 'still reports status when building the hint fails' {
        Mock Test-DFToolAvailable { $Executable -eq 'scoop.cmd' }
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        Mock New-DFInstallPlan { throw 'broken manager record' }
        (Get-DFToolStatus -Name glow 3>$null).State | Should -Be 'Missing'
    }
    It 'says how a missing tool would be installed' {
        Mock Test-DFToolAvailable { $Executable -eq 'scoop.cmd' }
        Start-DFSession -Config @{ Tools = @('glow') } -ToolsPath $script:Tools 3>$null
        (Get-DFToolStatus -Name glow).Detail | Should -Match 'Install-DFTool -Missing will install it via scoop'
    }
}