From a26138427c2614661f7e40363b86f81d779a5515 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 13 Jul 2026 13:47:20 -0400 Subject: [PATCH 1/2] Fix Compare-InstallerSnapshots.ps1 crash on empty entries Compare-InstallerSnapshots.ps1 runs under Set-StrictMode -Version Latest and read $p.DisplayName directly for every item in a snapshot's UninstallEntries. When a snapshot has no uninstall entries, Collect-InstallerSnapshot serializes the empty list as {} (a Windows PowerShell ConvertTo-Json quirk), so ConvertFrom-Json yields a single property-less object. Reading .DisplayName on it threw "The property 'DisplayName' cannot be found on this object", aborting the diff (observed after a silent MSI install whose before-snapshot had 0 entries). Extract DisplayName via a helper that tolerates $null, a single unwrapped object, and the empty {} object, skipping entries without a usable DisplayName. Verified: re-running against the real before/after snapshots that triggered the crash now completes (exit 0) and writes diff-before-vs-after-install.txt. Co-Authored-By: Claude Opus 4.8 --- scripts/Agent/Compare-InstallerSnapshots.ps1 | 32 +++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/scripts/Agent/Compare-InstallerSnapshots.ps1 b/scripts/Agent/Compare-InstallerSnapshots.ps1 index b6ab9e96d4..f896f794a8 100644 --- a/scripts/Agent/Compare-InstallerSnapshots.ps1 +++ b/scripts/Agent/Compare-InstallerSnapshots.ps1 @@ -54,6 +54,28 @@ function Diff-Sets { return [pscustomobject]@{ Added = $added; Removed = $removed } } +function Get-DisplayNameList { + # Safely extract DisplayName values from a snapshot's UninstallEntries. + # Robust to $null, to a single unwrapped object, and to the empty object ({}) + # that ConvertTo-Json can emit for an empty list (Collect-InstallerSnapshot serializes + # an empty List as {} in Windows PowerShell). Under Set-StrictMode, blindly reading + # $entry.DisplayName on such an object throws PropertyNotFoundException. + param([Parameter(Mandatory = $false)]$Entries) + + $names = New-Object System.Collections.Generic.List[string] + if ($null -eq $Entries) { return ,$names } + + foreach ($entry in @($Entries)) { + if ($null -eq $entry) { continue } + $prop = $entry.PSObject.Properties['DisplayName'] + if ($null -eq $prop) { continue } + $value = [string]$prop.Value + if (-not [string]::IsNullOrWhiteSpace($value)) { $names.Add($value) } + } + + return ,$names +} + function ConvertTo-PropertyMap { param([Parameter(Mandatory = $false)]$Value) @@ -86,14 +108,8 @@ $reportLines.Add("After: $AfterSnapshotPath") $reportLines.Add("") # Uninstall entries -$beforeProducts = @() -foreach ($p in ($before.UninstallEntries | ForEach-Object { $_ })) { - $beforeProducts += ([string]$p.DisplayName) -} -$afterProducts = @() -foreach ($p in ($after.UninstallEntries | ForEach-Object { $_ })) { - $afterProducts += ([string]$p.DisplayName) -} +$beforeProducts = Get-DisplayNameList -Entries $before.UninstallEntries +$afterProducts = Get-DisplayNameList -Entries $after.UninstallEntries $diffProducts = Diff-Sets -Before (To-StringSet -Items $beforeProducts) -After (To-StringSet -Items $afterProducts) $reportLines.Add("Uninstall entries") From 105148637af69341273a400b896fcad82a5d04f2 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 14 Aug 2026 03:00:02 -0400 Subject: [PATCH 2/2] Trim Get-DisplayNameList comment to the 200-char budget The comment above Get-DisplayNameList restated the function's name, named Collect-InstallerSnapshot as a cross-file provenance pointer, and ran well past the repo's 200-character implementation-comment budget. Trim it to the single non-obvious fact a reader needs: which input shapes StrictMode would otherwise throw on. --- scripts/Agent/Compare-InstallerSnapshots.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/Agent/Compare-InstallerSnapshots.ps1 b/scripts/Agent/Compare-InstallerSnapshots.ps1 index f896f794a8..55541ff091 100644 --- a/scripts/Agent/Compare-InstallerSnapshots.ps1 +++ b/scripts/Agent/Compare-InstallerSnapshots.ps1 @@ -55,11 +55,9 @@ function Diff-Sets { } function Get-DisplayNameList { - # Safely extract DisplayName values from a snapshot's UninstallEntries. - # Robust to $null, to a single unwrapped object, and to the empty object ({}) - # that ConvertTo-Json can emit for an empty list (Collect-InstallerSnapshot serializes - # an empty List as {} in Windows PowerShell). Under Set-StrictMode, blindly reading - # $entry.DisplayName on such an object throws PropertyNotFoundException. + # Tolerates $null, an unwrapped object, and the empty {} object Windows + # PowerShell emits for an empty list -- direct property access on those + # shapes throws under Set-StrictMode. param([Parameter(Mandatory = $false)]$Entries) $names = New-Object System.Collections.Generic.List[string]