tests/Build-DFToolIdentities.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../Private/Test-DFToolIdentityGuideSchema.ps1"
}

Describe 'Build-DFToolIdentities' {
    BeforeEach {
        $script:ToolsDir = Join-Path $TestDrive 'Tools'
        $script:IdentitiesDir = Join-Path $TestDrive 'identities'
        # $TestDrive is shared across every It in this Describe (Pester v5 does
        # not mint a fresh TestDrive per It), so fragment/tool files added by
        # one test would otherwise leak into later tests. Start each test from
        # clean dirs (matches the pattern in tests/Build-DFCategoryDb.Tests.ps1).
        if (Test-Path $script:ToolsDir) { Remove-Item $script:ToolsDir -Recurse -Force }
        if (Test-Path $script:IdentitiesDir) { Remove-Item $script:IdentitiesDir -Recurse -Force }
        New-Item -ItemType Directory -Path $script:ToolsDir -Force | Out-Null
        New-Item -ItemType Directory -Path $script:IdentitiesDir -Force | Out-Null
        $script:OutPath = Join-Path $TestDrive 'out.json'

        @{ name = 'ripgrep'; executable = 'rg.exe'; packages = @{ scoop = 'ripgrep'; winget = 'BurntSushi.ripgrep.MSVC' } } |
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:ToolsDir 'ripgrep.json')
        @{ name = 'nopkgs'; executable = 'nopkgs.exe' } |   # no packages block at all — must be skipped, not error
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:ToolsDir 'nopkgs.json')

        # A canned resolver: always reports the repo tier for ripgrep, curated for anything else.
        $script:CannedResolver = {
            param($ToolName, $Packages, $Fresh)
            if ($ToolName -eq 'ripgrep') { [pscustomobject]@{ LinkedVia = 'repo'; Repo = 'https://github.com/BurntSushi/ripgrep' } }
            else { [pscustomobject]@{ LinkedVia = 'curated'; Repo = $null } }
        }
    }

    It 'ingests Tools/*.json packages blocks, skipping tools with no packages block' {
        & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver
        $doc = Get-Content $script:OutPath -Raw | ConvertFrom-Json
        $doc.tools.PSObject.Properties.Name | Should -Contain 'ripgrep'
        $doc.tools.PSObject.Properties.Name | Should -Not -Contain 'nopkgs'
    }

    It 'records the resolver''s linkedVia and repo verbatim' {
        & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver
        $doc = Get-Content $script:OutPath -Raw | ConvertFrom-Json
        $doc.tools.ripgrep.linkedVia | Should -Be 'repo'
        $doc.tools.ripgrep.repo | Should -Be 'https://github.com/BurntSushi/ripgrep'
    }

    It 'merges hand-authored fragments alongside the Tools/*.json seed' {
        @{ zed = @{ packages = @{ winget = 'Zed.Zed' }; linkedVia = 'curated' } } |
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:IdentitiesDir 'extras.jsonc')
        & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver
        $doc = Get-Content $script:OutPath -Raw | ConvertFrom-Json
        $doc.tools.PSObject.Properties.Name | Should -Contain 'zed'
        $doc.tools.zed.linkedVia | Should -Be 'curated'
    }

    It 'strips // comments from fragment files' {
        @'
{
  // a comment
  "zed": { "packages": { "winget": "Zed.Zed" }, "linkedVia": "curated" }
}
'@
 | Set-Content (Join-Path $script:IdentitiesDir 'commented.jsonc')
        { & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver } |
            Should -Not -Throw
    }

    It 'throws on a duplicate tool key between a fragment and Tools/*.json' {
        @{ ripgrep = @{ packages = @{ npm = 'ripgrep' }; linkedVia = 'curated' } } |
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:IdentitiesDir 'dup.jsonc')
        { & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver } |
            Should -Throw '*ripgrep*'
    }

    It 'throws when a (source, packageId) pair is claimed by two different tool keys' {
        @{ ripgrep2 = @{ packages = @{ scoop = 'ripgrep' }; linkedVia = 'curated' } } |   # collides with ripgrep's scoop:ripgrep
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:IdentitiesDir 'collide.jsonc')
        { & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver } |
            Should -Throw '*scoop:ripgrep*'
    }

    It 'throws with the validator''s messages when the generated document is invalid' {
        $badResolver = { param($ToolName, $Packages, $Fresh) [pscustomobject]@{ LinkedVia = 'guessed'; Repo = $null } }
        { & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $badResolver } |
            Should -Throw '*linkedVia*'
    }

    It 'sorts tool keys alphabetically for a deterministic diff' {
        @{ aaa = @{ packages = @{ winget = 'Aaa.Aaa' }; linkedVia = 'curated' } } |
            ConvertTo-Json -Depth 6 | Set-Content (Join-Path $script:IdentitiesDir 'aaa.jsonc')
        & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver
        $raw = Get-Content $script:OutPath -Raw
        $raw.IndexOf('"aaa"') | Should -BeLessThan $raw.IndexOf('"ripgrep"')
    }

    It 'works with an empty (or absent) identities directory' {
        Remove-Item $script:IdentitiesDir -Recurse -Force
        { & "$PSScriptRoot/../build/Build-DFToolIdentities.ps1" -ToolsPath $script:ToolsDir -IdentitiesDir $script:IdentitiesDir -OutPath $script:OutPath -ResolveLinkage $script:CannedResolver } |
            Should -Not -Throw
    }
}

Describe 'the shipped data/tool-identities.json' {
    It 'validates against the schema' {
        $doc = Get-Content "$PSScriptRoot/../data/tool-identities.json" -Raw | ConvertFrom-Json
        $errs = $null
        Test-DFToolIdentityGuideSchema -Database $doc -Errors ([ref]$errs) | Should -BeTrue -Because ($errs -join '; ')
    }

    It 'contains every tool that has a packages block in Tools/*.json' {
        $doc = Get-Content "$PSScriptRoot/../data/tool-identities.json" -Raw | ConvertFrom-Json
        $curatedNames = Get-ChildItem "$PSScriptRoot/../Tools" -Filter '*.json' | ForEach-Object {
            $t = Get-Content $_.FullName -Raw | ConvertFrom-Json
            if ($t.packages -and @($t.packages.PSObject.Properties).Count -gt 0) { $t.name }
        } | Where-Object { $_ }
        foreach ($name in $curatedNames) {
            $doc.tools.PSObject.Properties.Name | Should -Contain $name
        }
    }
}