tests/Get-DFCachedCommandOutput.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../Private/ConvertTo-DFPath.ps1"
    . "$PSScriptRoot/../Public/New-DFDirectory.ps1"
    . "$PSScriptRoot/../Private/Get-DFCachedCommandOutput.ps1"
}

Describe 'Get-DFCachedCommandOutput' {
    BeforeEach {
        $script:SavedCacheHome = $Env:XDG_CACHE_HOME
        $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache'
        Remove-Item $Env:XDG_CACHE_HOME -Recurse -Force -ErrorAction Ignore

        # A real file stands in for the resolved executable so LastWriteTimeUtc
        # is real data, not a mocked property on a fake path.
        $script:FakeExe = Join-Path $TestDrive 'fake-tool.exe'
        Set-Content -Path $script:FakeExe -Value 'binary-stand-in' -Encoding UTF8
        Mock Get-Command { [PSCustomObject]@{ Source = $script:FakeExe } }
    }
    AfterEach {
        $Env:XDG_CACHE_HOME = $script:SavedCacheHome
    }

    It 'calls -Generate and caches the result on a cold cache' {
        $script:calls = 0
        $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate {
            $script:calls++
            'generated-output'
        }
        $result | Should -Be 'generated-output'
        $script:calls | Should -Be 1
        Test-Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.txt') | Should -BeTrue
    }

    It 'reuses the cache on a second call -- does not re-invoke -Generate' {
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'first' } | Out-Null

        $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate {
            throw '-Generate should not run on a cache hit'
        }
        $result | Should -Be 'first'
    }

    It 'regenerates when the executable file changes (LastWriteTimeUtc differs)' {
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'stale' } | Out-Null

        Start-Sleep -Milliseconds 50
        Set-Content -Path $script:FakeExe -Value 'a rebuilt binary' -Encoding UTF8

        $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'fresh' }
        $result | Should -Be 'fresh'
    }

    It 'regenerates when the resolved path changes even if the old file is untouched' {
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'from-old-path' } | Out-Null

        $newExe = Join-Path $TestDrive 'different-tool.exe'
        Set-Content -Path $newExe -Value 'binary-stand-in' -Encoding UTF8
        Mock Get-Command { [PSCustomObject]@{ Source = $newExe } }

        $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'from-new-path' }
        $result | Should -Be 'from-new-path'
    }

    It 'bypasses a valid cache when -Force is passed' {
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { 'first' } | Out-Null

        $result = Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Force -Generate { 'forced' }
        $result | Should -Be 'forced'
    }

    It 'never caches an empty result, and retries -Generate next call' {
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { '' } | Out-Null
        Test-Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge' 'test-tool.txt') | Should -BeFalse

        $script:calls = 0
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { $script:calls++; 'now-real' } | Out-Null
        $script:calls | Should -Be 1
    }

    It 'falls back to always calling -Generate, uncached, when the resolved command has no backing file (e.g. a function stand-in)' {
        # Regression: tests/scoop.Tests.ps1 stubs scoop-search as a PowerShell
        # function, not a real binary. Get-Command on a function does not
        # populate .Source with a usable file path -- Get-Item on it must not
        # throw, it must fall back to uncached generation.
        Mock Get-Command { [PSCustomObject]@{ Source = '' } }
        $script:calls = 0
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'stub-function' -Generate { $script:calls++; 'x' } | Out-Null
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'stub-function' -Generate { $script:calls++; 'x' } | Out-Null
        $script:calls | Should -Be 2
    }

    It 'falls back to always calling -Generate, uncached, when the executable does not resolve' {
        Mock Get-Command { $null }
        $script:calls = 0
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'missing-tool' -Generate { $script:calls++; 'x' } | Out-Null
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'missing-tool' -Generate { $script:calls++; 'x' } | Out-Null
        $script:calls | Should -Be 2
    }

    It 'falls back to always calling -Generate, uncached, when XDG_CACHE_HOME is unset' {
        $Env:XDG_CACHE_HOME = $null
        $script:calls = 0
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { $script:calls++; 'x' } | Out-Null
        Get-DFCachedCommandOutput -Name 'test-tool' -Executable 'test-tool' -Generate { $script:calls++; 'x' } | Out-Null
        $script:calls | Should -Be 2
    }

    It 'keeps separate cache entries for different -Name values' {
        Get-DFCachedCommandOutput -Name 'tool-a' -Executable 'test-tool' -Generate { 'output-a' } | Out-Null
        Get-DFCachedCommandOutput -Name 'tool-b' -Executable 'test-tool' -Generate { 'output-b' } | Out-Null

        Get-DFCachedCommandOutput -Name 'tool-a' -Executable 'test-tool' -Generate { throw 'should not run' } | Should -Be 'output-a'
        Get-DFCachedCommandOutput -Name 'tool-b' -Executable 'test-tool' -Generate { throw 'should not run' } | Should -Be 'output-b'
    }
}