tests/TabCompletionRole.Tests.ps1
|
BeforeAll { . "$PSScriptRoot/TestSupport.ps1" foreach ($f in Get-DFTestModuleFile) { . $f } } Describe 'tab-completion role declarations' { It 'uses the approved single-role definition and the three priorities' { $roles = Get-Content "$PSScriptRoot/../data/roles.json" -Raw | ConvertFrom-Json $roles.'tab-completion'.kind | Should -Be 'single' $roles.'tab-completion'.exclusive | Should -BeFalse (Get-Content "$PSScriptRoot/../Tools/PSFzf.json" -Raw | ConvertFrom-Json).roles.'tab-completion'.priority | Should -Be 20 (Get-Content "$PSScriptRoot/../Tools/carapace.json" -Raw | ConvertFrom-Json).roles.'tab-completion'.priority | Should -Be 10 $is = Get-Content "$PSScriptRoot/../Tools/inshellisense.json" -Raw | ConvertFrom-Json $is.roles.'tab-completion'.optIn | Should -BeTrue } It 'registers PSFzf after fzf, so fzf''s env block cannot wipe the --ansi PSFzf''s Tab hook adds' { # fzf.json sets FZF_DEFAULT_OPTS unconditionally; PSFzf's hook appends --ansi to it. (Get-Content "$PSScriptRoot/../Tools/PSFzf.json" -Raw | ConvertFrom-Json).requires | Should -Contain 'fzf' } } Describe 'core plugin invariant' { It 'does not branch on a literal shipped tool name in Private or Public code' { $root = Split-Path $PSScriptRoot -Parent $names = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) Get-ChildItem (Join-Path $root 'Tools') -Filter '*.json' | ForEach-Object { [void]$names.Add((Get-Content $_ -Raw | ConvertFrom-Json).name) } # Package-manager/catalog branches are deliberately deferred to audit item # #8. Keep this list exact: a new literal, even in one of these files, # fails below; stale entries also fail. $allowlist = @( [pscustomobject]@{ File = 'DFCatalog.Base.ps1'; Literal = 'scoop'; Reason = 'Scoop catalog bucket-qualified identity key' } [pscustomobject]@{ File = 'Find-DFPackage.ps1'; Literal = 'scoop'; Reason = 'Scoop catalog bucket-qualified query' } ) $occurrences = foreach ($file in Get-ChildItem (Join-Path $root 'Shared'), (Join-Path $root 'Private'), (Join-Path $root 'Public'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Private'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Public'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Private'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Public') -Filter '*.ps1') { $tokens = $null; $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors) $nodes = $ast.FindAll({ param($node) $isShippedName = { param($candidate) $candidate -is [System.Management.Automation.Language.StringConstantExpressionAst] -and $names.Contains($candidate.Value) } if ($node -is [System.Management.Automation.Language.BinaryExpressionAst]) { $leftHit = & $isShippedName $node.Left $rightHit = & $isShippedName $node.Right return $node.Operator.ToString() -match '^(?i:[ic]?(eq|ne|like|match|in|contains))$' -and ($leftHit -or $rightHit) } if ($node -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) { return $node.Member.Value -match '^(?i:Contains|ContainsKey|Equals|StartsWith)$' -and @($node.Arguments | Where-Object { & $isShippedName $_ }).Count -gt 0 } if ($node -is [System.Management.Automation.Language.SwitchStatementAst]) { foreach ($clause in $node.Clauses) { if (@($clause.Item1.FindAll({ param($part) & $isShippedName $part }, $true)).Count -gt 0) { return $true } } } $false }, $true) foreach ($node in $nodes) { $literals = if ($node -is [System.Management.Automation.Language.BinaryExpressionAst]) { @($node.Left, $node.Right) } elseif ($node -is [System.Management.Automation.Language.InvokeMemberExpressionAst]) { @($node.Arguments) } else { foreach ($clause in $node.Clauses) { $clause.Item1.FindAll({ param($part) $part -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) } } foreach ($literal in @($literals | Where-Object { $_ -is [System.Management.Automation.Language.StringConstantExpressionAst] -and $names.Contains($_.Value) })) { [pscustomobject]@{ File = $file.Name; Literal = $literal.Value; Text = $node.Extent.Text } } } } $unallowed = @($occurrences | Where-Object { $occurrence = $_ -not @($allowlist | Where-Object { $_.File -eq $occurrence.File -and $_.Literal -ieq $occurrence.Literal }).Count }) $stale = @($allowlist | Where-Object { $entry = $_ -not @($occurrences | Where-Object { $_.File -eq $entry.File -and $_.Literal -ieq $entry.Literal }).Count }) @($unallowed | ForEach-Object { "$($_.File):$($_.Literal):$($_.Text)" }) | Should -BeNullOrEmpty -Because 'core code must remain tool-agnostic outside the catalog/package-manager audit allowlist' @($stale | ForEach-Object { "$($_.File):$($_.Literal)" }) | Should -BeNullOrEmpty -Because 'every deferred audit allowlist entry must still match real code' } } |