tests/DFCatalog.Core.Tests.ps1
|
BeforeAll { . "$PSScriptRoot/../Public/New-DFDirectory.ps1" . "$PSScriptRoot/../Private/DFCatalog.ps1" } Describe 'Get-DFCatalogCacheRoot' { BeforeEach { $script:SavedXdgCache = $Env:XDG_CACHE_HOME } AfterEach { $Env:XDG_CACHE_HOME = $script:SavedXdgCache } It 'returns catalogs dir under XDG_CACHE_HOME/dotforge' { $Env:XDG_CACHE_HOME = $TestDrive Get-DFCatalogCacheRoot | Should -Be (Join-Path $TestDrive 'dotforge/catalogs') } It 'warns and returns $null when XDG_CACHE_HOME is unset' { $Env:XDG_CACHE_HOME = $null $result = Get-DFCatalogCacheRoot -WarningVariable warnings -WarningAction SilentlyContinue $result | Should -BeNullOrEmpty $warnings | Should -Not -BeNullOrEmpty } } Describe 'ConvertTo-DFCatalogQueryKey' { It 'normalizes case, trim, and internal whitespace' { $r = ConvertTo-DFCatalogQueryKey -Query " Static Site`tGenerator " $r.Normalized | Should -Be 'static site generator' } It 'produces a filename-safe key with an 8-hex hash suffix' { $r = ConvertTo-DFCatalogQueryKey -Query 'static site generator' $r.Key | Should -Match '^static_site_generator-[0-9a-f]{8}$' } It 'sanitizes characters outside [a-z0-9._-]' { $r = ConvertTo-DFCatalogQueryKey -Query 'C++ IDE!' $r.Key | Should -Match '^[a-z0-9._-]+-[0-9a-f]{8}$' } It 'truncates the readable part to 40 chars but stays unique via the hash' { $long = 'x' * 100 $r = ConvertTo-DFCatalogQueryKey -Query $long ($r.Key -replace '-[0-9a-f]{8}$').Length | Should -BeLessOrEqual 40 $r2 = ConvertTo-DFCatalogQueryKey -Query ('x' * 101) $r2.Key | Should -Not -Be $r.Key } It 'is deterministic' { (ConvertTo-DFCatalogQueryKey -Query 'ripgrep').Key | Should -Be (ConvertTo-DFCatalogQueryKey -Query 'ripgrep').Key } } Describe 'Write-DFCatalogCacheFile / Read-DFCatalogCacheFile' { BeforeEach { $script:SavedXdgCache = $Env:XDG_CACHE_HOME $Env:XDG_CACHE_HOME = $TestDrive } AfterEach { $Env:XDG_CACHE_HOME = $script:SavedXdgCache } It 'round-trips an envelope of results' { $path = Join-Path $TestDrive 'dotforge/catalogs/npm/queries/ripgrep-abcd1234.json' $results = @([pscustomobject]@{ Name = 'ripgrep'; LatestVersion = '14.1.1' }) Write-DFCatalogCacheFile -Path $path -Query 'ripgrep' -Results $results $r = Read-DFCatalogCacheFile -Path $path -Ttl ([timespan]::FromHours(24)) $r.Data.Count | Should -Be 1 $r.Data[0].Name | Should -Be 'ripgrep' $r.Data[0].LatestVersion | Should -Be '14.1.1' $r.Stale | Should -BeFalse $r.AgeMinutes | Should -BeLessOrEqual 1 } It 'leaves no temp files behind' { $path = Join-Path $TestDrive 'dotforge/catalogs/npm/queries/tmp-test.json' Write-DFCatalogCacheFile -Path $path -Query 'q' -Results @() Get-ChildItem (Split-Path $path) -Filter '*.tmp.*' | Should -BeNullOrEmpty } It 'reports Stale when the envelope is older than the TTL' { $path = Join-Path $TestDrive 'stale.json' $old = [datetime]::UtcNow.AddHours(-48).ToString('o') Set-Content -Path $path -Value (@{ timestamp = $old; query = 'q'; results = @() } | ConvertTo-Json) -Encoding UTF8 $r = Read-DFCatalogCacheFile -Path $path -Ttl ([timespan]::FromHours(24)) $r.Stale | Should -BeTrue $r.AgeMinutes | Should -BeGreaterThan (47 * 60) } It 'returns $null for a missing file' { Read-DFCatalogCacheFile -Path (Join-Path $TestDrive 'nope.json') -Ttl ([timespan]::FromHours(1)) | Should -BeNullOrEmpty } It 'returns $null for a corrupt file' { $path = Join-Path $TestDrive 'corrupt.json' Set-Content -Path $path -Value 'not json {' -Encoding UTF8 Read-DFCatalogCacheFile -Path $path -Ttl ([timespan]::FromHours(1)) | Should -BeNullOrEmpty } } Describe 'New-DFToolSourceInfo / New-DFToolInfo' { It 'stamps DotForge.ToolSourceInfo with source fields' { $s = New-DFToolSourceInfo -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' ` -Description 'search tool' -LatestVersion '14.1.1' -MatchKind 'exact-name' $s.PSObject.TypeNames[0] | Should -Be 'DotForge.ToolSourceInfo' $s.Source | Should -Be 'scoop' $s.PackageId | Should -Be 'main/ripgrep' $s.Installed | Should -BeFalse $s.MatchKind | Should -Be 'exact-name' } It 'stamps DotForge.ToolInfo carrying its sources' { $s = New-DFToolSourceInfo -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' -MatchKind 'exact-name' $i = New-DFToolInfo -Name 'ripgrep' -Description 'search tool' -Sources @($s) $i.PSObject.TypeNames[0] | Should -Be 'DotForge.ToolInfo' $i.Name | Should -Be 'ripgrep' $i.Sources.Count | Should -Be 1 $i.Installed | Should -BeFalse } } Describe 'Get-DFCatalogProvider' { BeforeEach { $script:SavedProviders = $script:DFCatalogProviders $script:DFCatalogProviders = @{} $script:DFCatalogAvailability = @{} # Global (not $script: + GetNewClosure) — GetNewClosure would rebind # $script: to a cloned dynamic module and the increments would vanish. $global:DFTestProbeCount = 0 $script:DFCatalogProviders['crates'] = @{ Name = 'crates'; Kind = 'query-cache'; Test = { $true } } $script:DFCatalogProviders['scoop'] = @{ Name = 'scoop'; Kind = 'snapshot' Test = { $global:DFTestProbeCount++; $true } } $script:DFCatalogProviders['npm'] = @{ Name = 'npm'; Kind = 'query-cache'; Test = { $false } } } AfterEach { $script:DFCatalogProviders = $script:SavedProviders $script:DFCatalogAvailability = @{} Remove-Variable -Name DFTestProbeCount -Scope Global -ErrorAction Ignore } It 'returns available providers in canonical order' { $p = Get-DFCatalogProvider $p.Name | Should -Be @('scoop', 'crates') # canonical order, npm filtered (Test=$false) } It 'filters by -Source' { $p = Get-DFCatalogProvider -Source 'crates' $p.Name | Should -Be @('crates') } It 'memoizes availability probes for the session' { $null = Get-DFCatalogProvider $null = Get-DFCatalogProvider $global:DFTestProbeCount | Should -Be 1 } } Describe 'Add-DFCatalogSeenQuery' { BeforeEach { $script:SavedXdgCache = $Env:XDG_CACHE_HOME $Env:XDG_CACHE_HOME = $TestDrive } AfterEach { $Env:XDG_CACHE_HOME = $script:SavedXdgCache } It 'persists queries most-recent-first without duplicates' { Add-DFCatalogSeenQuery -Query 'aaa' Add-DFCatalogSeenQuery -Query 'bbb' Add-DFCatalogSeenQuery -Query 'aaa' $file = Join-Path $TestDrive 'dotforge/catalogs/seen-queries.json' $seen = Get-Content $file -Raw | ConvertFrom-Json @($seen).Count | Should -Be 2 @($seen)[0].query | Should -Be 'aaa' @($seen)[1].query | Should -Be 'bbb' } It 'caps the list at 50 entries, dropping the oldest' { 1..55 | ForEach-Object { Add-DFCatalogSeenQuery -Query "q$_" } $file = Join-Path $TestDrive 'dotforge/catalogs/seen-queries.json' $seen = Get-Content $file -Raw | ConvertFrom-Json @($seen).Count | Should -Be 50 @($seen)[0].query | Should -Be 'q55' @($seen).query | Should -Not -Contain 'q1' } It 'is a no-op without XDG_CACHE_HOME' { $Env:XDG_CACHE_HOME = $null { Add-DFCatalogSeenQuery -Query 'x' -WarningAction SilentlyContinue } | Should -Not -Throw } } |