tests/DFPackageUniverse.Links.Tests.ps1
|
BeforeAll { Import-Module PSSQLite # ConvertTo-DFNormalizedHomepage is reused from the existing identity code. . "$PSScriptRoot/../Private/Resolve-DFToolIdentityCandidateRepo.ps1" . "$PSScriptRoot/../build/Private/DFPackageUniverse.Db.ps1" . "$PSScriptRoot/../build/Private/DFPackageUniverse.Links.ps1" # A raw_packages-shaped row. extra is a JSON string (as stored), source-specific. function New-RawRow { param( [string]$Source, [string]$PackageId, [string]$Name = 'tool', [string]$Publisher = $null, [string]$Homepage = $null, $Extra = $null ) [pscustomobject]@{ source = $Source; package_id = $PackageId; name = $Name; version = '1.0' description = 'd'; homepage = $Homepage; license = 'MIT'; publisher = $Publisher tags = $null; extra = ($Extra ? (ConvertTo-Json -Compress -Depth 8 -InputObject $Extra) : $null) } } function Edge($edges, $method) { @($edges | Where-Object { $_.Method -eq $method }) } function E($sa, $ia, $sb, $ib, $m, $c) { [pscustomobject]@{ SourceA = $sa; PackageIdA = $ia; SourceB = $sb; PackageIdB = $ib; Method = $m; Confidence = $c; Evidence = 'e' } } function Node($s, $p) { [pscustomobject]@{ source = $s; package_id = $p } } function Seed($db, $source, $packageId, $name, $homepage) { Invoke-SqliteQuery -DataSource $db -Query "INSERT INTO raw_packages (source, package_id, name, version, homepage, fetched_at) VALUES (@s, @p, @n, '1', @h, 'now')" -SqlParameters @{ s = $source; p = $packageId; n = $name; h = $homepage } } } Describe 'DFPackageUniverse.Links' { Context 'Get-DFPackageUniverseRepoKey' { It 'derives owner/repo from a github homepage, lowercased' { $row = New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'burntsushi/ripgrep' } It 'strips a trailing .git and any path/query/fragment' { $row = New-RawRow -Source 'winget' -PackageId 'X.Y' -Homepage 'https://github.com/Owner/Repo.git#readme' Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'owner/repo' } It 'returns $null for a non-github homepage with no repo in extra' { $row = New-RawRow -Source 'winget' -PackageId 'X.Y' -Homepage 'https://vendor.example/product' Get-DFPackageUniverseRepoKey -Row $row | Should -BeNullOrEmpty } It 'recovers the repo from choco ProjectSourceUrl when homepage is a vanity domain' { $row = New-RawRow -Source 'choco' -PackageId 'tool' -Homepage 'https://vanity.example/tool' -Extra @{ ProjectUrl = 'https://vanity.example/tool'; ProjectSourceUrl = 'https://github.com/acme/tool' } Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'acme/tool' } It 'does NOT treat choco PackageSourceUrl (the packaging repo) as the tool repo' { # PackageSourceUrl points at the chocolatey packaging repo, shared by # thousands of packages -- using it would fuse them all (trifle zed at scale). $row = New-RawRow -Source 'choco' -PackageId 'tool' -Homepage 'https://vanity.example/tool' -Extra @{ PackageSourceUrl = 'https://github.com/chocolatey-community/chocolatey-packages' } Get-DFPackageUniverseRepoKey -Row $row | Should -BeNullOrEmpty } It 'recovers the repo from a scoop checkver.github object when homepage is a vanity domain' { $row = New-RawRow -Source 'scoop' -PackageId 'main/tool' -Homepage 'https://vanity.example/tool' -Extra @{ version = '1.0'; homepage = 'https://vanity.example/tool' checkver = @{ github = 'https://github.com/acme/tool' } } Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'acme/tool' } It 'prefers choco ProjectSourceUrl (source repo) over a differing github homepage (docs repo)' { $row = New-RawRow -Source 'choco' -PackageId 'tool' -Homepage 'https://github.com/acme/tool-docs' -Extra @{ ProjectSourceUrl = 'https://github.com/acme/tool' ProjectUrl = 'https://github.com/acme/tool-docs' } Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'acme/tool' } It 'recovers the repo from scoop autoupdate urls when homepage is a vanity domain and checkver is absent' { $row = New-RawRow -Source 'scoop' -PackageId 'main/tool' -Homepage 'https://vanity.example/tool' -Extra @{ version = '1.0' autoupdate = @{ architecture = @{ '64bit' = @{ url = 'https://github.com/acme/tool/releases/download/v$version/tool.zip' } } } } Get-DFPackageUniverseRepoKey -Row $row | Should -Be 'acme/tool' } It 'does not throw under strict mode on a row with null homepage and null extra' { Set-StrictMode -Version Latest $row = New-RawRow -Source 'winget' -PackageId 'X.Y' { Get-DFPackageUniverseRepoKey -Row $row } | Should -Not -Throw Get-DFPackageUniverseRepoKey -Row $row | Should -BeNullOrEmpty } } Context 'normalization' { It 'ConvertTo-DFNormalizedName lowercases and strips non-alphanumeric' { ConvertTo-DFNormalizedName -Value 'WMI Explorer' | Should -Be 'wmiexplorer' ConvertTo-DFNormalizedName -Value '7-Zip' | Should -Be '7zip' } It 'ConvertTo-DFNormalizedName returns empty string for null or empty input' { ConvertTo-DFNormalizedName -Value $null | Should -Be '' ConvertTo-DFNormalizedName -Value '' | Should -Be '' } It 'ConvertTo-DFNormalizedPublisher strips corporate suffixes and punctuation' { ConvertTo-DFNormalizedPublisher -Value 'DWANGO Co., Ltd.' | Should -Be 'dwango' ConvertTo-DFNormalizedPublisher -Value '3CX Software DMCC' | Should -Be '3cx' ConvertTo-DFNormalizedPublisher -Value 'Vinay Pamnani' | Should -Be 'vinaypamnani' } It 'ConvertTo-DFNormalizedPublisher returns empty for null (scoop rows have no publisher)' { ConvertTo-DFNormalizedPublisher -Value $null | Should -Be '' } } Context 'ConvertTo-DFPackageUniverseBaseName' { It 'strips the .install suffix from a choco name' { ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name '7zip.install' | Should -Be '7zip' } It 'strips .install / .portable / .commandline suffixes, case-insensitively' { ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name '7zip.install' | Should -Be '7zip' ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name '7zip.portable' | Should -Be '7zip' ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name '7zip.commandline' | Should -Be '7zip' ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name 'GitDepend.Portable' | Should -Be 'GitDepend' } It 'leaves a choco name without such a suffix unchanged' { ConvertTo-DFPackageUniverseBaseName -Source 'choco' -Name 'ripgrep' | Should -Be 'ripgrep' } It 'leaves a non-choco name with a .portable-looking suffix unchanged (choco-only strip)' { ConvertTo-DFPackageUniverseBaseName -Source 'winget' -Name 'X.portable' | Should -Be 'X.portable' } } Context 'Get-DFPackageUniverseLinkKeys' { It 'returns a repo key and no homepage key for a github homepage' { $row = New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' $keys = Get-DFPackageUniverseLinkKeys -Row $row $keys.Repo | Should -Be 'burntsushi/ripgrep' $keys.Homepage | Should -BeNullOrEmpty $keys.Name | Should -Be 'ripgrep' } It 'returns a normalized homepage key (scheme/www/trailing-slash stripped) for a non-github vanity url with a path' { $row = New-RawRow -Source 'winget' -PackageId 'V.P' -Name 'Product' -Publisher 'Vendor' -Homepage 'https://www.Vendor.com/Product/' $keys = Get-DFPackageUniverseLinkKeys -Row $row $keys.Homepage | Should -Be 'vendor.com/product' $keys.Repo | Should -BeNullOrEmpty } It 'emits no homepage key for a bare-domain homepage (too weak; left to review)' { $row = New-RawRow -Source 'winget' -PackageId 'V.P' -Name 'Product' -Homepage 'https://vendor.com' (Get-DFPackageUniverseLinkKeys -Row $row).Homepage | Should -BeNullOrEmpty } It 'builds a publisher-name key only when a publisher is present (scoop has none)' { $wg = New-RawRow -Source 'winget' -PackageId 'V.P' -Name 'Tool' -Publisher 'Acme' (Get-DFPackageUniverseLinkKeys -Row $wg).PubName | Should -Be 'acme|tool' $sc = New-RawRow -Source 'scoop' -PackageId 'main/tool' -Name 'Tool' (Get-DFPackageUniverseLinkKeys -Row $sc).PubName | Should -BeNullOrEmpty } It 'gates the name-only key to names of length >= 4' { (Get-DFPackageUniverseLinkKeys -Row (New-RawRow -Source 'choco' -PackageId 'fd' -Name 'fd')).Name | Should -BeNullOrEmpty (Get-DFPackageUniverseLinkKeys -Row (New-RawRow -Source 'choco' -PackageId 'ripgrep' -Name 'ripgrep')).Name | Should -Be 'ripgrep' } It 'normalizes chocolatey packaging-variant suffixes to the same Name key as the base tool (7zip / 7zip.install)' { $base = New-RawRow -Source 'choco' -PackageId '7zip' -Name '7zip' $variant = New-RawRow -Source 'choco' -PackageId '7zip.install' -Name '7zip.install' (Get-DFPackageUniverseLinkKeys -Row $base).Name | Should -Be (Get-DFPackageUniverseLinkKeys -Row $variant).Name } } Context 'Get-DFPackageUniverseLinkEdges' { It 'emits a repo edge (confidence 1.0) between two catalogs sharing a github repo' { $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' New-RawRow -Source 'winget' -PackageId 'BurntSushi.ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' ) $repo = Edge (Get-DFPackageUniverseLinkEdges -Rows $rows) 'repo' $repo.Count | Should -Be 1 $repo[0].Confidence | Should -Be 1.0 $repo[0].Evidence | Should -Be 'burntsushi/ripgrep' } It 'emits a homepage edge (0.75) for a shared normalized non-github homepage' { $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/tool' -Name 'Tool' -Homepage 'https://vendor.example/tool' New-RawRow -Source 'choco' -PackageId 'tool' -Name 'Tool' -Homepage 'https://vendor.example/tool/' ) $hp = Edge (Get-DFPackageUniverseLinkEdges -Rows $rows) 'homepage' $hp.Count | Should -Be 1 $hp[0].Confidence | Should -Be 0.75 } It 'emits a publisher-name edge (0.60) when pub+name agree and repos do not conflict' { $rows = @( New-RawRow -Source 'winget' -PackageId 'Acme.Tool' -Name 'Tool' -Publisher 'Acme' New-RawRow -Source 'choco' -PackageId 'tool' -Name 'Tool' -Publisher 'Acme Inc' ) $edges = Get-DFPackageUniverseLinkEdges -Rows $rows (Edge $edges 'publisher-name').Count | Should -Be 1 (Edge $edges 'publisher-name')[0].Confidence | Should -Be 0.60 } It 'downgrades a publisher-name group to a conflict edge (0.30) when members resolve to different repos' { $rows = @( New-RawRow -Source 'winget' -PackageId 'lucasg.Dependencies' -Name 'Dependencies' -Publisher 'lucasg' -Homepage 'https://github.com/himeshsameera/dependencies' New-RawRow -Source 'choco' -PackageId 'dependencies' -Name 'Dependencies' -Publisher 'lucasg' -Homepage 'https://github.com/lucasg/dependencies' ) $edges = Get-DFPackageUniverseLinkEdges -Rows $rows (Edge $edges 'publisher-name').Count | Should -Be 0 (Edge $edges 'conflict').Count | Should -Be 1 (Edge $edges 'conflict')[0].Confidence | Should -Be 0.30 } It 'emits a name-only edge (0.20) for a shared name when nothing else links or conflicts' { $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/7zip' -Name '7zip' New-RawRow -Source 'choco' -PackageId '7zip' -Name '7zip' ) $edges = Get-DFPackageUniverseLinkEdges -Rows $rows (Edge $edges 'name-only').Count | Should -Be 1 (Edge $edges 'name-only')[0].Confidence | Should -Be 0.20 } It 'downgrades a name-only group to conflict (0.30) when members resolve to different repos (cemu emulators)' { # 'cemu' (len 4) collides two different projects: the Wii-U emulator # and the calculator emulator. Different repos -> conflict, not merge. $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/cemu' -Name 'cemu' -Homepage 'https://github.com/cemu-project/cemu' New-RawRow -Source 'winget' -PackageId 'CE-Programming.CEmu' -Name 'CEmu' -Homepage 'https://github.com/ce-programming/cemu' ) $edges = Get-DFPackageUniverseLinkEdges -Rows $rows (Edge $edges 'name-only').Count | Should -Be 0 (Edge $edges 'conflict').Count | Should -Be 1 } It 'emits no edges for a single unmatched row' { $rows = @(New-RawRow -Source 'scoop' -PackageId 'main/solo' -Name 'solo' -Homepage 'https://github.com/x/solo') @(Get-DFPackageUniverseLinkEdges -Rows $rows).Count | Should -Be 0 } It 'does not auto-merge a monorepo family: many distinctly-named packages sharing one repo emit no repo edges' { $rows = @(1..6 | ForEach-Object { New-RawRow -Source 'scoop' -PackageId "nerd-fonts/Font$_" -Name "Font$_-NF" -Homepage 'https://github.com/ryanoasis/nerd-fonts' }) (Edge (Get-DFPackageUniverseLinkEdges -Rows $rows) 'repo').Count | Should -Be 0 } It 'still clusters a small same-repo variant group below the family size threshold (ripgrep GNU/MSVC)' { $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' New-RawRow -Source 'winget' -PackageId 'BurntSushi.ripgrep.MSVC' -Name 'RipGrep MSVC' -Homepage 'https://github.com/BurntSushi/ripgrep' New-RawRow -Source 'choco' -PackageId 'ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' ) (Edge (Get-DFPackageUniverseLinkEdges -Rows $rows) 'repo').Count | Should -BeGreaterThan 0 } } Context 'Test-DFPackageUniverseFamily' { It 'flags a single-source multi-name group as a family even below the size threshold (OpenDsc vendor cluster)' { $rows = @( New-RawRow -Source 'winget' -PackageId 'OpenDsc.Lcm' -Name 'OpenDsc Lcm' New-RawRow -Source 'winget' -PackageId 'OpenDsc.Resources' -Name 'OpenDsc Resources' New-RawRow -Source 'winget' -PackageId 'OpenDsc.Server' -Name 'OpenDsc Server' ) $members = Get-DFPackageUniverseKeyedRows -Rows $rows Test-DFPackageUniverseFamily -Members $members -Threshold 5 | Should -BeTrue } It 'does not flag a cross-source multi-name group below the size threshold (protects the ripgrep-shape merge)' { $rows = @( New-RawRow -Source 'winget' -PackageId 'BurntSushi.ripgrep.MSVC' -Name 'ripgrep MSVC' New-RawRow -Source 'choco' -PackageId 'ripgrep' -Name 'ripgrep' New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep GNU' ) $members = Get-DFPackageUniverseKeyedRows -Rows $rows Test-DFPackageUniverseFamily -Members $members -Threshold 5 | Should -BeFalse } It 'does not flag a single-source group with only one distinct name (variant/repackage of one tool)' { $rows = @( New-RawRow -Source 'winget' -PackageId 'A.X' -Name 'Tool' New-RawRow -Source 'winget' -PackageId 'A.Y' -Name 'Tool' ) $members = Get-DFPackageUniverseKeyedRows -Rows $rows Test-DFPackageUniverseFamily -Members $members -Threshold 5 | Should -BeFalse } } Context 'Get-DFPackageUniverseFamilyGroups' { It 'surfaces a monorepo (shared repo, many distinct names) for review' { $rows = @(1..6 | ForEach-Object { New-RawRow -Source 'scoop' -PackageId "nerd-fonts/Font$_" -Name "Font$_-NF" -Homepage 'https://github.com/ryanoasis/nerd-fonts' }) $fam = @(Get-DFPackageUniverseFamilyGroups -Rows $rows) $fam.Count | Should -Be 1 $fam[0].Tier | Should -Be 'repo' $fam[0].Key | Should -Be 'ryanoasis/nerd-fonts' $fam[0].Size | Should -Be 6 } It 'surfaces a shared-homepage vendor family (distinct names, one url) for review' { $rows = @(1..6 | ForEach-Object { New-RawRow -Source 'winget' -PackageId "V.P$_" -Name "Product$_" -Homepage 'https://dot.net/core' }) $fam = @(Get-DFPackageUniverseFamilyGroups -Rows $rows) $fam.Count | Should -Be 1 $fam[0].Tier | Should -Be 'homepage' } It 'does not flag a small variant group as a family' { $rows = @( New-RawRow -Source 'scoop' -PackageId 'main/ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' New-RawRow -Source 'choco' -PackageId 'ripgrep' -Name 'ripgrep' -Homepage 'https://github.com/BurntSushi/ripgrep' ) @(Get-DFPackageUniverseFamilyGroups -Rows $rows).Count | Should -Be 0 } } Context 'Invoke-DFPackageUniverseClustering' { It 'merges nodes connected by above-threshold edges into one cluster' { $edges = @( (E 'scoop' 'a' 'winget' 'b' 'repo' 1.0) (E 'winget' 'b' 'choco' 'c' 'repo' 1.0) ) $clusters = @(Invoke-DFPackageUniverseClustering -Edges $edges) $clusters.Count | Should -Be 1 $clusters[0].Members.Count | Should -Be 3 $clusters[0].MinConfidence | Should -Be 1.0 } It 'does not cluster on a below-threshold edge (name-only 0.20)' { $edges = @((E 'scoop' 'a' 'choco' 'b' 'name-only' 0.20)) @(Invoke-DFPackageUniverseClustering -Edges $edges).Count | Should -Be 0 } It 'force-merges a curated confirmed-same group even without any edges' { $clusters = @(Invoke-DFPackageUniverseClustering -Edges @() -CuratedSame @(, @((Node 'scoop' 'a'), (Node 'choco' 'b')))) $clusters.Count | Should -Be 1 $clusters[0].Members.Count | Should -Be 2 $clusters[0].HasCurated | Should -BeTrue } It 'blocks a merge forbidden by a curated cannot-link pair' { $edges = @((E 'scoop' 'air' 'winget' 'air' 'repo' 1.0)) $clusters = @(Invoke-DFPackageUniverseClustering -Edges $edges -CuratedDifferent @(, @((Node 'scoop' 'air'), (Node 'winget' 'air')))) @($clusters | Where-Object { $_.Members.Count -ge 2 }).Count | Should -Be 0 } It 'flags a cluster needsReview when a conflict edge touches it' { $edges = @( (E 'scoop' 'a' 'winget' 'b' 'repo' 1.0) (E 'winget' 'b' 'choco' 'c' 'conflict' 0.30) ) $clusters = @(Invoke-DFPackageUniverseClustering -Edges $edges) $clusters[0].NeedsReview | Should -BeTrue } } Context 'Import-DFPackageUniverseCuration' { It 'parses same-groups and different-pairs from a JSONC file with comments' { $path = Join-Path $TestDrive 'cur.jsonc' Set-Content -Path $path -Encoding UTF8 -Value @' { // curated cross-catalog identities "schemaVersion": 1, "same": [ { "id": "bat", "members": [ { "source": "scoop", "package_id": "bat" }, { "source": "winget", "package_id": "sharkdp.bat" } ] } ], "different": [ { "note": "R formatter vs Go reloader", "members": [ { "source": "scoop", "package_id": "main/air" }, { "source": "winget", "package_id": "Posit.Air" } ] } ] } '@ $cur = Import-DFPackageUniverseCuration -Path $path $cur.Same.Count | Should -Be 1 $cur.Same[0].Count | Should -Be 2 $cur.Same[0][0].source | Should -Be 'scoop' $cur.Same[0][1].package_id | Should -Be 'sharkdp.bat' $cur.Different.Count | Should -Be 1 $cur.Different[0].Count | Should -Be 2 } It 'does not corrupt a package_id containing a slash (line-comment strip is anchored)' { $path = Join-Path $TestDrive 'slash.jsonc' Set-Content -Path $path -Encoding UTF8 -Value @' { "same": [ { "members": [ { "source": "scoop", "package_id": "main/7zip" }, { "source": "choco", "package_id": "7zip" } ] } ], "different": [] } '@ (Import-DFPackageUniverseCuration -Path $path).Same[0][0].package_id | Should -Be 'main/7zip' } It 'returns empty same/different for a missing file' { $cur = Import-DFPackageUniverseCuration -Path (Join-Path $TestDrive 'nope.jsonc') $cur.Same.Count | Should -Be 0 $cur.Different.Count | Should -Be 0 } } Context 'Links DB layer' { BeforeEach { $script:Db = Join-Path $TestDrive "links-$([guid]::NewGuid().ToString('N')).db" Initialize-DFPackageUniverseDb -DatabasePath $script:Db | Out-Null Initialize-DFPackageUniverseLinksSchema -DatabasePath $script:Db } It 'creates the four link tables' { $tables = @((Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT name FROM sqlite_master WHERE type='table'").name) foreach ($t in 'identity_links', 'identity_clusters', 'cluster_members', 'curated_links') { $tables | Should -Contain $t } } It 'persists edges, clusters, members, the curated mirror, and review-log rows' { $edges = @( (E 'scoop' 'a' 'winget' 'b' 'repo' 1.0) (E 'scoop' 'x' 'choco' 'y' 'name-only' 0.20) ) $clusters = @(Invoke-DFPackageUniverseClustering -Edges $edges) $curation = [pscustomobject]@{ Same = @(); Different = @(, @((Node 'scoop' 'air'), (Node 'winget' 'air'))) } $conn = New-SQLiteConnection -DataSource $script:Db try { Save-DFPackageUniverseLinks -Connection $conn -Edges $edges -Clusters $clusters -Curation $curation } finally { $conn.Close() } (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM identity_links").c | Should -Be 2 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM identity_clusters").c | Should -Be 1 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM cluster_members").c | Should -Be 2 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM curated_links WHERE kind='different'").c | Should -Be 2 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM pipeline_log WHERE stage='link' AND level='review'").c | Should -BeGreaterThan 0 } It 're-running the schema init clears prior link rows (per-run truncation)' { $e = @((E 'scoop' 'a' 'winget' 'b' 'repo' 1.0)) $conn = New-SQLiteConnection -DataSource $script:Db try { Save-DFPackageUniverseLinks -Connection $conn -Edges $e -Clusters @(Invoke-DFPackageUniverseClustering -Edges $e) -Curation ([pscustomobject]@{ Same = @(); Different = @() }) } finally { $conn.Close() } Initialize-DFPackageUniverseLinksSchema -DatabasePath $script:Db (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM identity_links").c | Should -Be 0 } } Context 'Invoke-DFPackageUniverseLinkBuild (end-to-end)' { BeforeEach { $script:Db = Join-Path $TestDrive "e2e-$([guid]::NewGuid().ToString('N')).db" Initialize-DFPackageUniverseDb -DatabasePath $script:Db | Out-Null Initialize-DFPackageUniverseLinksSchema -DatabasePath $script:Db } It 'clusters the bat ground-truth trio (scoop/winget/choco) via a shared repo' { Seed $script:Db 'scoop' 'bat' 'bat' 'https://github.com/sharkdp/bat' Seed $script:Db 'winget' 'sharkdp.bat' 'bat' 'https://github.com/sharkdp/bat' Seed $script:Db 'choco' 'bat' 'bat' 'https://github.com/sharkdp/bat' $conn = New-SQLiteConnection -DataSource $script:Db try { $summary = Invoke-DFPackageUniverseLinkBuild -Connection $conn -CurationPath (Join-Path $TestDrive 'none.jsonc') } finally { $conn.Close() } $summary.RowsRead | Should -Be 3 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(DISTINCT cluster_id) c FROM cluster_members").c | Should -Be 1 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM cluster_members").c | Should -Be 3 } It 'keeps a curated confirmed-different pair split even when a repo signal would merge them' { # Two 'air' packages pointing at the SAME repo would merge by repo (1.0); # a curated 'different' cannot-link must keep them apart. Seed $script:Db 'scoop' 'main/air' 'air' 'https://github.com/air-verse/air' Seed $script:Db 'winget' 'Posit.Air' 'air' 'https://github.com/air-verse/air' $cur = Join-Path $TestDrive 'cur.jsonc' Set-Content -Path $cur -Encoding UTF8 -Value '{ "same": [], "different": [ { "members": [ {"source":"scoop","package_id":"main/air"}, {"source":"winget","package_id":"Posit.Air"} ] } ] }' $conn = New-SQLiteConnection -DataSource $script:Db try { Invoke-DFPackageUniverseLinkBuild -Connection $conn -CurationPath $cur } finally { $conn.Close() } (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM cluster_members").c | Should -Be 0 } It 'flags a monorepo family for review instead of clustering it' { 1..6 | ForEach-Object { Seed $script:Db 'scoop' "nerd-fonts/Font$_" "Font$_-NF" 'https://github.com/ryanoasis/nerd-fonts' } $conn = New-SQLiteConnection -DataSource $script:Db try { $s = Invoke-DFPackageUniverseLinkBuild -Connection $conn -CurationPath (Join-Path $TestDrive 'none.jsonc') } finally { $conn.Close() } (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM cluster_members").c | Should -Be 0 (Invoke-SqliteQuery -DataSource $script:Db -Query "SELECT COUNT(*) c FROM pipeline_log WHERE stage='link' AND level='review' AND message LIKE '%family%'").c | Should -BeGreaterThan 0 $s.Families | Should -Be 1 } } } |