Fix Show Installed Apps reliability and selection (#4850)

* Fix Windows PowerShell runspace cleanup callback

* Fix installed app package ID matching

* Synchronize installed app selection UI

* Streamline installed app detection

* Avoid installed app UI runspace deadlock
This commit is contained in:
Chris Titus
2026-07-16 13:30:12 -05:00
committed by GitHub
parent b9dee86694
commit 06f33e88ba
8 changed files with 281 additions and 47 deletions
+4
View File
@@ -140,6 +140,10 @@ Describe "Invoke-WPFRunspace behavior" {
$runspaceScript | Should -Not -Match '\$script:powershell'
$runspaceScript | Should -Not -Match '\$script:handle'
}
It "exposes a strongly typed cleanup callback" {
([WinUtilRunspaceCleanup]::Callback -is [System.Threading.WaitOrTimerCallback]) | Should -BeTrue
}
}
Describe "Public runspace callers" {
+70
View File
@@ -6,12 +6,82 @@ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
BeforeAll {
$script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
. (Join-Path $script:repoRoot "functions\private\Invoke-WinUtilCurrentSystem.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilRegistry.ps1")
. (Join-Path $script:repoRoot "functions\private\Set-WinUtilService.ps1")
function winget {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function choco {
param([Parameter(ValueFromRemainingArguments = $true)]$Arguments)
}
function Write-WinUtilLog { }
}
Describe "Invoke-WinUtilCurrentSystem installed apps" {
BeforeEach {
$script:sync = [Hashtable]::Synchronized(@{
configs = [pscustomobject]@{
applicationsHashtable = @{
WPFInstallGit = [pscustomobject]@{ winget = "Git.Git"; choco = "git" }
WPFInstallChatGPT = [pscustomobject]@{ winget = "msstore:9NT1R1C2HH7J"; choco = "na" }
WPFInstallMissing = [pscustomobject]@{ winget = "Git"; choco = "missing" }
}
}
})
Mock winget {
$global:LASTEXITCODE = 0
$script:wingetArguments = @($Arguments)
@(
"Name Id Version Source",
"--------------------------------",
"Git Git.Git 2.0 winget",
"ChatGPT 9NT1R1C2HH7J 1.0 msstore"
)
}
Mock choco {
$script:chocoArguments = @($Arguments)
@("Chocolatey v2", "git 2.0", "2 packages installed.")
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name wingetArguments -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name chocoArguments -Scope Script -ErrorAction SilentlyContinue
}
It "matches single standard and Microsoft Store package IDs" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "winget")
$result | Should -HaveCount 2
$result | Should -Contain "WPFInstallGit"
$result | Should -Contain "WPFInstallChatGPT"
$result | Should -Not -Contain "WPFInstallMissing"
Should -Invoke -CommandName winget -Times 1 -Exactly
$script:wingetArguments | Should -Be @("list", "--accept-source-agreements", "--disable-interactivity")
}
It "fails promptly when Winget cannot list applications" {
Mock winget {
$global:LASTEXITCODE = 1
"winget failed"
}
{ Invoke-WinUtilCurrentSystem -CheckBox "winget" } | Should -Throw "winget list failed with exit code 1."
}
It "matches the primary Chocolatey package ID in one list call" {
$result = @(Invoke-WinUtilCurrentSystem -CheckBox "choco")
$result | Should -Be @("WPFInstallGit")
Should -Invoke -CommandName choco -Times 1 -Exactly
$script:chocoArguments | Should -Be @("list")
}
}
Describe "Set-WinUtilRegistry" {
BeforeEach {
$script:testPathResults = @{}
+110
View File
@@ -61,6 +61,7 @@ namespace System.Windows.Controls
. (Join-Path $script:repoRoot "functions\private\Update-WinUtilSelections.ps1")
. (Join-Path $script:repoRoot "functions\private\Reset-WPFCheckBoxes.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFGetInstalled.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFSelectedCheckboxesUpdate.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFButton.ps1")
. (Join-Path $script:repoRoot "functions\public\Invoke-WPFToggleAllCategories.ps1")
@@ -68,6 +69,24 @@ namespace System.Windows.Controls
function Set-WinUtilTweaksProgressIndicator {
param($Visible, $Label, $Percent)
}
function Invoke-WPFRunspace {
param($ArgumentList, $ParameterList, [scriptblock]$ScriptBlock)
}
function Invoke-WPFUIThread {
param([scriptblock]$ScriptBlock)
}
function Invoke-WinUtilCurrentSystem {
param($CheckBox)
}
function Set-WinUtilTaskbaritem {
param($state)
}
function Test-WinUtilPackageManager {
param([switch]$winget)
}
function Write-WinUtilLog {
param($Message, $Level, $Component)
}
function script:New-WinUtilFakeCheckBox {
param([bool]$IsChecked = $false)
@@ -180,6 +199,9 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
@($script:sync.selectedToggles) | Should -Be @("WPFToggleDarkMode")
@($script:sync.selectedFeatures) | Should -Be @("WPFFeatureSandbox")
@($script:sync.selectedAppx) | Should -Be @("WPFAppxExample")
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "removes checkbox keys from the matching selected lists" {
@@ -200,6 +222,94 @@ Describe "Invoke-WPFSelectedCheckboxesUpdate" {
$script:sync.selectedToggles.Count | Should -Be 0
$script:sync.selectedFeatures.Count | Should -Be 0
$script:sync.selectedAppx.Count | Should -Be 0
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 0"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 0
}
}
Describe "Invoke-WPFGetInstalled selection state" {
BeforeEach {
New-WinUtilUiStateTestContext
$script:sync.ProcessRunning = $false
$script:sync.ChocoRadioButton = [pscustomobject]@{ IsChecked = $false }
$script:sync.preferences = [pscustomobject]@{ packagemanager = "Winget" }
$script:sync.WPFInstallGit = New-WinUtilFakeCheckBox
$dispatcher = [pscustomobject]@{}
$dispatcher | Add-Member -MemberType ScriptMethod -Name BeginInvoke -Value {
param($Action, [object[]]$Arguments)
$Action.DynamicInvoke($Arguments)
}
$script:sync.Form = [pscustomobject]@{ Dispatcher = $dispatcher }
$script:capturedGetInstalledScriptBlock = $null
$script:capturedGetInstalledParameters = @{}
Mock Test-WinUtilPackageManager { "installed" }
Mock Invoke-WinUtilCurrentSystem { @("WPFInstallGit") }
Mock Set-WinUtilTaskbaritem { }
Mock Write-WinUtilLog { }
Mock Write-Warning { }
Mock Invoke-WPFRunspace {
$script:capturedGetInstalledScriptBlock = $ScriptBlock
foreach ($parameter in $ParameterList) {
$script:capturedGetInstalledParameters[$parameter[0]] = $parameter[1]
}
}
}
AfterEach {
Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name sync -Scope Global -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledScriptBlock -Scope Script -ErrorAction SilentlyContinue
Remove-Variable -Name capturedGetInstalledParameters -Scope Script -ErrorAction SilentlyContinue
}
It "updates the selected app model, checkbox, count, and popup" {
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
@($script:sync.selectedApps) | Should -Be @("WPFInstallGit")
$script:sync.WPFInstallGit.IsChecked | Should -BeTrue
$script:sync.WPFselectedAppsButton.Content | Should -Be "Selected Apps: 1"
$script:sync.selectedAppsstackPanel.Children.Count | Should -Be 1
$script:sync.selectedAppsstackPanel.Children[0].Key | Should -Be "WPFInstallGit"
}
It "clears the running state when detection fails" {
Mock Invoke-WinUtilCurrentSystem { throw "detection failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
& $script:capturedGetInstalledScriptBlock `
-checkbox "winget" `
-managerPreference "Winget" `
-operation $script:capturedGetInstalledParameters.operation `
-completeAction $script:capturedGetInstalledParameters.completeAction
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: detection failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
}
It "clears the running state when the worker cannot be queued" {
Mock Invoke-WPFRunspace { throw "queue failed" }
Invoke-WPFGetInstalled -CheckBox "winget"
$script:sync.ProcessRunning | Should -BeFalse
Should -Invoke -CommandName Write-WinUtilLog -Times 1 -Exactly -ParameterFilter {
$Level -eq "ERROR" -and
$Component -eq "Install" -and
$Message -eq "Get installed state failed: queue failed"
}
Should -Invoke -CommandName Set-WinUtilTaskbaritem -Times 1 -Exactly -ParameterFilter { $state -eq "None" }
}
}
-1
View File
@@ -435,7 +435,6 @@ Describe "XAML and sync wiring" {
"appPopup",
"appPopupSelectedApp",
"ItemsControl",
"InstalledPrograms",
"ImportInProgress",
"ScriptsInstallPrograms",
"keys",