tests/DefaultToolRoles.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/TestSupport.ps1"
    foreach ($f in Get-DFTestModuleFile) { . $f }
    $script:RealTools = Join-Path $PSScriptRoot '../Tools'
}

Describe 'eza/lsd share role: listing (real tool records)' {
    BeforeEach { Reset-DFTestSession;
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:DFRoleDb = $null
        Set-DFTestConfig $null
        $script:SavedXdg = @{}
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            $script:SavedXdg[$v] = [Environment]::GetEnvironmentVariable("XDG_$($v)_HOME")
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", (Join-Path $TestDrive "$($v.ToLower())-$([guid]::NewGuid())"))
        }
        Mock Get-Command {
            param($Name)
            if ($Name -in 'eza.exe', 'lsd.exe') { [PSCustomObject]@{ Path = "C:\fake\$Name" } }
        }
        Mock Write-DFConflictNotice { }
        # Stand-ins so a wrapper's `& eza ...` / `& lsd ...` resolves to a capturable
        # function (Function resolves before Application), whether or not the real
        # binaries are installed. A .GetNewClosure() wrapper's ScriptBlock.ToString()
        # shows only the unbound template, so calling it is the only reliable probe.
        function global:eza { $global:LastCommandCalled = 'eza' }
        function global:lsd { $global:LastCommandCalled = 'lsd' }
    }
    AfterEach {
        Set-DFTestConfig $null; Remove-Variable LastCommandCalled -Scope Global -ErrorAction Ignore
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", $script:SavedXdg[$v])
        }
        # eza's real picker block also defines Select-File and its ff alias.
        Remove-DFTestGlobal -Function ls, ll, la, tree, Select-File, eza, lsd -Alias ff
    }

    It 'declares listing on both tools, with the listing aliases inside the role block' {
        foreach ($name in 'eza', 'lsd') {
            $j = Get-Content (Join-Path $script:RealTools "$name.json") -Raw | ConvertFrom-Json
            $j.roles.listing.aliases.PSObject.Properties.Name | Should -Be @('ls', 'll', 'la', 'tree')
            @($j.aliases.PSObject.Properties).Count | Should -Be 0
            $j.PSObject.Properties['role'] | Should -BeNullOrEmpty
        }
    }

    It 'gives the listing aliases to <Winner> when Defaults.listing = <Winner>' -ForEach @(@{ Winner = 'eza' }, @{ Winner = 'lsd' }) {
        Set-DFTestConfig @{ Defaults = @{ listing = $Winner } }
        Register-DFTool -Name 'eza', 'lsd' -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        foreach ($a in 'ls', 'll', 'la', 'tree') { & $a; $global:LastCommandCalled | Should -Be $Winner -Because $a }
    }

    It 'gives the listing aliases to eza by priority when Defaults is absent' {
        Register-DFTool -Name 'eza', 'lsd' -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        & 'ls'; $global:LastCommandCalled | Should -Be 'eza'
    }
}

Describe 'oh-my-posh/starship share role: prompt (real tool records and sidecars)' {
    BeforeAll { . "$PSScriptRoot/../Private/Get-DFCachedCommandOutput.ps1" }
    BeforeEach { Reset-DFTestSession;
        # These tests fake "installed" with Mock Get-Command / Get-Module; route the PATH and module probes through them.
        Mock Test-DFExecutableOnPath { [bool](Get-Command $Name -ErrorAction Ignore) }
        Mock Test-DFModuleOnPath { [bool](Get-Module -ListAvailable -Name $Name -ErrorAction Ignore) }
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:DFRoleDb = $null
        $global:DFRoleEnvState = $null
        Set-DFTestConfig $null
        $script:SavedXdg = @{}
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            $script:SavedXdg[$v] = [Environment]::GetEnvironmentVariable("XDG_$($v)_HOME")
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", (Join-Path $TestDrive "$($v.ToLower())-$([guid]::NewGuid())"))
        }
        $script:SavedPoshTheme = $Env:POSH_THEME
        Remove-Item Env:POSH_THEME -ErrorAction Ignore
        # One oh-my-posh config for the sidecar to discover.
        $ompDir = Join-Path $Env:XDG_CONFIG_HOME 'oh-my-posh'
        New-Item -ItemType Directory -Force $ompDir | Out-Null
        '{}' | Set-Content (Join-Path $ompDir 'test.omp.json')

        $global:PromptInits = @()
        Mock Get-Command {
            param($Name)
            if ($Name -in 'oh-my-posh.exe', 'starship.exe') { [PSCustomObject]@{ Path = "C:\fake\$Name" } }
        }
        Mock Get-Module { }   # no posh-git, so the oh-my-posh hook skips its import
        Mock Write-DFConflictNotice { }
        Mock Get-DFCachedCommandOutput { '$global:PromptInits += "starship"' } -ParameterFilter { $Name -eq 'starship-init' }
        # oh-my-posh's init output is piped to Invoke-Expression; emit a line that records the call.
        function global:oh-my-posh { '$global:PromptInits += "oh-my-posh"' }
    }
    AfterEach {
        Set-DFTestConfig $null; Remove-Variable PromptInits -Scope Global -ErrorAction Ignore
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", $script:SavedXdg[$v])
        }
        if ($null -eq $script:SavedPoshTheme) { Remove-Item Env:POSH_THEME -ErrorAction Ignore } else { $Env:POSH_THEME = $script:SavedPoshTheme }
        Remove-Item Env:POSH_THEMES_PATH, Env:STARSHIP_CONFIG, Env:STARSHIP_CACHE -ErrorAction Ignore
        Remove-DFTestGlobal -Function oh-my-posh, Select-PoshTheme -Alias fpot
    }

    It 'initializes only oh-my-posh by priority, defines fpot, and warns once' {
        Register-DFTool -Name 'oh-my-posh', 'starship' -ToolsPath $script:RealTools -WarningVariable w -WarningAction SilentlyContinue
        $global:PromptInits | Should -Be @('oh-my-posh')
        (Get-Alias fpot -ErrorAction Ignore).Definition | Should -Be 'Select-PoshTheme'
        "$w" | Should -Match 'prompt role; using oh-my-posh'
    }

    It 'initializes only starship when Defaults.prompt = starship, and leaves fpot undefined' {
        Set-DFTestConfig @{ Defaults = @{ prompt = 'starship' } }
        Register-DFTool -Name 'oh-my-posh', 'starship' -ToolsPath $script:RealTools -WarningVariable w -WarningAction SilentlyContinue
        $global:PromptInits | Should -Be @('starship')
        Get-Alias fpot -ErrorAction Ignore | Should -BeNullOrEmpty
        "$w" | Should -Not -Match 'prompt role'
    }
}

Describe 'ps-dotenv/mise/direnv share role: project-env (real tool records and sidecars)' {
    BeforeAll {

        $fakeDir = Join-Path $TestDrive 'fake-dotenv\Dotenv'
        New-Item -ItemType Directory -Force $fakeDir | Out-Null
        $script:FakeDotenv = Join-Path $fakeDir 'Dotenv.psm1'
        @'
$Dotenv = [pscustomobject]@{ Enabled = $false; SafeMode = $false; Async = $true }
function Enable-Dotenv { $Dotenv.Enabled = $true }
function Approve-DotenvDir { param([Parameter(Mandatory)][string]$Path) }
function Update-Dotenv { $global:ProjectEnvInits += 'ps-dotenv' }
Export-ModuleMember -Function * -Variable Dotenv
'@
 | Set-Content $script:FakeDotenv
    }
    BeforeEach { Reset-DFTestSession;
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:DFRoleDb = $null
        $global:DFRoleEnvState = $null
        Set-DFTestConfig $null; Remove-Variable DFDotenvLocationHook -Scope Global -ErrorAction Ignore
        $script:SavedXdg = @{}
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            $script:SavedXdg[$v] = [Environment]::GetEnvironmentVariable("XDG_$($v)_HOME")
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", (Join-Path $TestDrive "$($v.ToLower())-$([guid]::NewGuid())"))
        }
        $script:SavedPath = $Env:Path
        $script:SavedLca = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction
        $global:ProjectEnvInits = @()
        Mock Get-Command {
            param($Name)
            if ($Name -in 'direnv.exe', 'mise.exe') { [PSCustomObject]@{ Path = "C:\fake\$Name" } }
        }
        Mock Get-Module { [pscustomobject]@{ Name = 'Dotenv'; Path = $script:FakeDotenv } } -ParameterFilter { $ListAvailable }
        Mock Write-DFConflictNotice { }
        Mock Get-DFCachedCommandOutput { '$global:ProjectEnvInits += "direnv"' } -ParameterFilter { $Name -eq 'direnv-hook' }
        Mock Get-DFCachedCommandOutput { '2.38.0' } -ParameterFilter { $Name -eq 'direnv-version' }
        function global:mise { '$global:ProjectEnvInits += "mise"' }
    }
    AfterEach {
        $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $script:SavedLca
        $Env:Path = $script:SavedPath
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", $script:SavedXdg[$v])
        }
        Remove-Module Dotenv -Force -ErrorAction Ignore
        Set-DFTestConfig $null; Remove-Variable DFDotenvLocationHook, ProjectEnvInits, Dotenv, DFRoleEnvState -Scope Global -ErrorAction Ignore
        Remove-Item Env:DIRENV_BASH -ErrorAction Ignore
        Remove-DFTestGlobal -Function mise
    }

    It 'activates only ps-dotenv by priority, warns once, and keeps mise''s shims on PATH' {
        Register-DFTool -Name 'ps-dotenv', 'mise', 'direnv' -ToolsPath $script:RealTools -WarningVariable w -WarningAction SilentlyContinue
        $global:ProjectEnvInits | Should -Be @('ps-dotenv')
        @($w | Where-Object { "$_" -match 'can each fill the project-env role' }).Count | Should -Be 1
        "$w" | Should -Match 'project-env role; using ps-dotenv'
        "$w" | Should -Match "'project-env' = 'mise'" -Because 'the notice suggests the runner-up by priority'
        ($Env:Path -split ';') | Should -Contain (Join-Path $Env:XDG_DATA_HOME 'mise\shims')
    }

    It 'activates only mise when Defaults.project-env = mise' {
        Set-DFTestConfig @{ Defaults = @{ 'project-env' = 'mise' } }
        Register-DFTool -Name 'ps-dotenv', 'mise', 'direnv' -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        $global:ProjectEnvInits | Should -Be @('mise')
    }
}

Describe 'moor/ov/less share role: pager (real tool records)' {
    BeforeEach { Reset-DFTestSession;
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:DFRoleDb = $null
        $global:DFRoleEnvState = $null
        Set-DFTestConfig $null
        $script:SavedXdg = @{}
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            $script:SavedXdg[$v] = [Environment]::GetEnvironmentVariable("XDG_$($v)_HOME")
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", (Join-Path $TestDrive "$($v.ToLower())-$([guid]::NewGuid())"))
        }
        $script:SavedPager = $Env:PAGER
        $script:SavedMoor = $Env:MOOR
        Remove-Item Env:PAGER, Env:MOOR -ErrorAction Ignore
        Mock Get-Command {
            param($Name)
            switch ($Name) {
                'less.exe' { [pscustomobject]@{ Source = 'C:\Program Files\Git\usr\bin\less.exe'; Path = 'C:\Program Files\Git\usr\bin\less.exe' }
                             [pscustomobject]@{ Source = 'C:\scoop\shims\less.exe'; Path = 'C:\scoop\shims\less.exe' } }
                { $_ -in 'moor.exe', 'ov.exe', 'bat.exe' } { [pscustomobject]@{ Source = "C:\fake\$Name"; Path = "C:\fake\$Name" } }
            }
        }
        Mock Write-DFConflictNotice { }
    }
    AfterEach {
        foreach ($v in 'CONFIG', 'CACHE', 'STATE', 'DATA') {
            [Environment]::SetEnvironmentVariable("XDG_$($v)_HOME", $script:SavedXdg[$v])
        }
        if ($null -eq $script:SavedPager) { Remove-Item Env:PAGER -ErrorAction Ignore } else { $Env:PAGER = $script:SavedPager }
        if ($null -eq $script:SavedMoor) { Remove-Item Env:MOOR -ErrorAction Ignore } else { $Env:MOOR = $script:SavedMoor }
        Set-DFTestConfig $null; Remove-Variable DFRoleEnvState -Scope Global -ErrorAction Ignore
        Remove-DFTestGlobal -Function cat -Alias cat
    }

    It 'makes moor the pager by priority' {
        Register-DFTool -Name moor, ov, less, bat -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        $Env:PAGER | Should -Be 'moor'
    }

    It 'uses ov with quit-if-one-screen when Defaults.pager = ov' {
        Set-DFTestConfig @{ Defaults = @{ pager = 'ov' } }
        Register-DFTool -Name moor, ov, less, bat -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        $Env:PAGER | Should -Be 'ov --quit-if-one-screen'
    }

    It 'points PAGER at the native less, skipping Git''s MSYS build, when Defaults.pager = less' {
        Set-DFTestConfig @{ Defaults = @{ pager = 'less' } }
        Register-DFTool -Name moor, ov, less, bat -ToolsPath $script:RealTools -WarningAction SilentlyContinue
        $Env:PAGER | Should -Be 'C:/scoop/shims/less.exe'
    }

    It 'no longer counts bat as a pager' {
        $j = Get-Content (Join-Path $script:RealTools 'bat.json') -Raw | ConvertFrom-Json
        $j.PSObject.Properties['roles'] | Should -BeNullOrEmpty
    }
}