Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:

- name: Verify, build, test, and package
shell: powershell
run: .\build\Build.ps1 -Configuration Release -Version 2.0.0-beta.1-ci
run: .\build\Build.ps1 -Configuration Release -Version 3.0.0-beta.1-ci

- name: Upload test results
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

- name: Build analyzed source
shell: powershell
run: .\build\Build.ps1 -Configuration Release -Version 2.0.0-beta.1-codeql -SkipTests
run: .\build\Build.ps1 -Configuration Release -Version 3.0.0-beta.1-codeql -SkipTests

- name: Analyze
uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
46 changes: 34 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,42 @@ This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) an

## [Unreleased]

- Replace the PSWindowsUpdate wrapper with an independent typed Windows Update Agent engine.
- Add same-executable GUI and CLI modes with versioned JSON results and stable exit codes.
- Add GUID-and-revision update selection, async WUA progress/cancellation, offline scans, services, policy, jobs, reporting, maintenance, and payload export.
- Stage the same verified executable for remote WinRM operations instead of installing a module.
- Remove the embedded PSWindowsUpdate package and all runtime module extraction/import code.
- Preserve the former 19-command feature surface as an explicit operation-migration matrix.
## [3.0.0-beta.1] - 2026-07-22

### Changed

- Migrate the complete desktop shell from WPF/.NET Framework 4.8 to WinUI 3 on .NET
10 and Windows App SDK 2.3.1.
- Publish an unpackaged, framework-self-contained, single-file Windows 11 x64 EXE;
no permanent runtime, package, or update module installation is required.
- Replace the WPF dispatcher dependency in the WUA worker with an owning native STA
message pump and move fixed WinRM transport to built-in Windows PowerShell 5.1.
- Redesign History & Status with compact status cards, an inline typed history table,
selected-entry details, and exact-revision removal or driver rollback verification.
- Add native System, Light, and Dark WinUI themes, Mica, themed title bar, modern
controls, semantic resources, and Windows high-contrast fallback.

### Added

- Portable elevated Windows 11 WPF application.
- Guided local and native remote update workflows.
- Metadata-driven coverage of all PSWindowsUpdate 2.2.1.5 public cmdlets.
- Signed vendor verification, redacted logging, tests, SBOM, CI, and release automation.
- Add a production-assembly WinUI smoke mode with a fake WUA adapter, test-only
non-elevated manifest, dark-theme screenshot, and no machine mutations.
- Pin .NET 10, Windows App SDK 2.3.1, and Windows SDK BuildTools 10.0.28000.2270.

### Removed

- Remove WPF, .NET Framework, GAC PowerShell automation assembly, and the obsolete
cross-assembly UI smoke host from the product and build.

## [1.0.0] - Unreleased

- Initial public release after clean local and remote VM acceptance.
## [2.0.0-beta.1] - 2026-07-21

### Added

- Portable, elevated Windows 11 x64 WPF application and same-executable CLI.
- Typed Windows Update Agent engine with asynchronous search, download, install, and
uninstall operations selected by update GUID and revision.
- Software and driver workflows, update history and status, service management,
Microsoft-signed offline catalogs, payload export, policy backup and restore,
component maintenance, scheduled jobs, SMTP reporting, and secure remote execution.
- Versioned JSON results, stable exit codes, mutation plans, explicit confirmation
gates, redacted logs, locked dependencies, tests, SPDX SBOM, CI, and release provenance.
17 changes: 0 additions & 17 deletions PSWindowsUpdateGUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSWindowsUpdateGui.Acceptance", "tests\PSWindowsUpdateGui.Acceptance\PSWindowsUpdateGui.Acceptance.csproj", "{909A2504-A985-4D80-B925-3A104B7E0B04}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSWindowsUpdateGui.UiSmoke", "tests\PSWindowsUpdateGui.UiSmoke\PSWindowsUpdateGui.UiSmoke.csproj", "{D467FC9B-27B2-4E66-B582-CC94D6F39950}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -61,24 +57,11 @@ Global
{909A2504-A985-4D80-B925-3A104B7E0B04}.Release|Any CPU.Build.0 = Release|x64
{909A2504-A985-4D80-B925-3A104B7E0B04}.Release|x86.ActiveCfg = Release|x64
{909A2504-A985-4D80-B925-3A104B7E0B04}.Release|x86.Build.0 = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|x64.ActiveCfg = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|x64.Build.0 = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|Any CPU.ActiveCfg = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|Any CPU.Build.0 = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|x86.ActiveCfg = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Debug|x86.Build.0 = Debug|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|x64.ActiveCfg = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|x64.Build.0 = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|Any CPU.ActiveCfg = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|Any CPU.Build.0 = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|x86.ActiveCfg = Release|x64
{D467FC9B-27B2-4E66-B582-CC94D6F39950}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{909A2504-A985-4D80-B925-3A104B7E0B04} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{D467FC9B-27B2-4E66-B582-CC94D6F39950} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# PSWindowsUpdate GUI

PSWindowsUpdate GUI 2 is an independent, portable Windows 11 x64 graphical and
command-line interface for Windows Update Agent (WUA). It does **not** install,
load, or wrap the PSWindowsUpdate module.
PSWindowsUpdate GUI 3 is a portable Windows 11 x64 graphical and command-line
administrator tool built directly on Windows Update Agent (WUA). The WinUI 3 release
is a self-contained single executable; it does not install a runtime or update module.

> [!WARNING]
> This is an administrator tool. Installing or uninstalling updates, changing
Expand All @@ -14,11 +14,17 @@ load, or wrap the PSWindowsUpdate module.

_Screenshot uses the repository's non-mutating UI smoke adapter and synthetic host identity._

![Dark History and Status page](docs/images/gui-history-dark.png)

## Highlights

- One elevated .NET Framework 4.8 executable; Windows 11 already includes its runtime.
- One elevated, unpackaged, self-contained WinUI 3 executable for Windows 11 x64.
- Direct typed WUA engine with GUID-and-revision update selection.
- Software and driver scan, download, install, uninstall, hide, and unhide workflows.
- Structured status cards and update history with exact-revision removal checks and
guarded uninstall or driver rollback when Windows reports the update as removable.
- Native WinUI 3 shell with NavigationView, Mica, modern controls, persisted System,
Light, and Dark themes, title-bar theming, keyboard focus, and high-contrast fallback.
- Asynchronous WUA progress, timeout, abort requests, structured HRESULTs, and reboot state.
- Online Windows Update, Microsoft Update, managed WSUS, registered service, and
Microsoft-signed `wsusscn2.cab` offline scan sources.
Expand Down Expand Up @@ -50,7 +56,9 @@ new process from the calling console:
.\PSWindowsUpdateGUI.exe status --computer workstation.contoso.test --use-ssl --output json
```

Launching without arguments opens WPF. See [CLI reference](docs/CLI.md),
Launching without arguments opens WinUI 3. Select `System`, `Light`, or `Dark` from the
header; `System` follows Windows app-theme and high-contrast changes. See the
[interface and theming notes](docs/UI.md), [CLI reference](docs/CLI.md),
[remote administration](docs/REMOTE.md), and [security model](docs/SECURITY.md).

The implementation follows Microsoft's [WUA object model](https://learn.microsoft.com/windows/win32/wua_sdk/windows-update-agent-object-model),
Expand All @@ -67,25 +75,29 @@ and [remote interface restrictions](https://learn.microsoft.com/windows/win32/wu
- GUI mutations show a summary. CLI mutations prompt for `YES`; redirected input requires `--yes`.
- Offline CABs only assess security-update applicability and do not contain payloads.
- WUA-driven operations may not appear in the Windows Settings orchestrator UI.
- Update history is an immutable audit record. Removing an installed update does not
delete its history, and a driver downgrade requires a previous Windows-managed package.

## Build

```powershell
$env:DOTNET_EXE = (Resolve-Path .\.tools\dotnet\dotnet.exe)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build\Build.ps1 -Version 2.0.0-beta.1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build\Build.ps1 -Version 3.0.0-beta.1
```

The build verifies Microsoft Authenticode on the system WUA DLL, generates an
interop reference from its installed type library, embeds only referenced COM types,
runs tests, and emits the EXE, checksum, SPDX SBOM, and notices under
`artifacts\release`. No interop DLL or PSWindowsUpdate package is released.
runs tests plus a production-XAML smoke capture, and emits the single-file EXE,
checksum, SPDX SBOM, and notices under `artifacts\release`. The first start extracts
self-contained framework files to the .NET bundle cache; it does not install them.
No interop DLL or third-party update engine is released.

## Project status

Version 2 is a major prerelease until local and remote snapshot-backed Windows 11 VM
acceptance is complete. The physical-machine acceptance runner is read-only unless
both an exact driver identity and `--confirm-machine-mutation` are supplied.
Version `3.0.0-beta.1` is the current major prerelease. Promotion to a stable release
remains gated on local and remote snapshot-backed Windows 11 VM acceptance. The
physical-machine acceptance runner is read-only unless both an exact driver identity
and `--confirm-machine-mutation` are supplied.

This project is MIT licensed. Repository history before 2.0 used PSWindowsUpdate
2.2.1.5 under its separate MIT license; 2.x contains none of its files. This project
is independent and is not affiliated with the upstream PSWindowsUpdate project.
The application source is MIT licensed. Release contents and their licenses are
recorded in the generated SPDX SBOM and `THIRD-PARTY-NOTICES.txt`.
15 changes: 11 additions & 4 deletions THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
PSWindowsUpdateGUI 2.x does not bundle PSWindowsUpdate or another update engine.
PSWindowsUpdateGUI does not redistribute a third-party update engine.

The self-contained executable redistributes the Microsoft .NET 10 runtime, Windows
App SDK 2.3.1 runtime files, and System.ServiceProcess.ServiceController 10.0. These
Microsoft open-source components are distributed under the MIT License. Their exact
package versions and relationships are recorded in the accompanying SPDX SBOM.

The application uses Windows Update Agent (wuapi.dll), Windows PowerShell 5.1 for
authenticated WinRM transport, and other Windows components already present on the
supported operating system. Those operating-system components are not redistributed.

Repository history prior to 2.0 contains an embedded copy of PSWindowsUpdate 2.2.1.5,
copyright Michal Gajda and licensed under the MIT License. No PSWindowsUpdate files are
present in 2.x source or release assets.
Project and license information:

- https://github.com/dotnet/runtime
- https://github.com/microsoft/WindowsAppSDK
- https://licenses.nuget.org/MIT
37 changes: 27 additions & 10 deletions build/Build.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[CmdletBinding()]
param(
[ValidateSet('Debug','Release')] [string] $Configuration = 'Release',
[string] $Version = '2.0.0-beta.1',
[string] $Version = '3.0.0-beta.1',
[switch] $SkipTests
)

Expand All @@ -17,25 +17,42 @@ function Invoke-DotNet {

& (Join-Path $PSScriptRoot 'Verify-Native.ps1')
Invoke-DotNet restore (Join-Path $repoRoot 'PSWindowsUpdateGUI.sln') --use-lock-file '-p:Platform=x64'
Invoke-DotNet build (Join-Path $repoRoot 'PSWindowsUpdateGUI.sln') -c $Configuration "-p:Version=$Version" '-p:Platform=x64' --no-restore
Invoke-DotNet clean (Join-Path $repoRoot 'PSWindowsUpdateGUI.sln') -c $Configuration '-p:Platform=x64' --verbosity minimal
$generatedManifest = Join-Path $repoRoot "src\PSWindowsUpdateGui\obj\x64\$Configuration\net10.0-windows10.0.26100.0\win-x64\Manifests\app.manifest"
if (Test-Path -LiteralPath $generatedManifest) { Remove-Item -LiteralPath $generatedManifest -Force }
Invoke-DotNet build (Join-Path $repoRoot 'PSWindowsUpdateGUI.sln') -c $Configuration "-p:Version=$Version" '-p:Platform=x64' --no-restore --no-incremental
if (-not $SkipTests) {
Invoke-DotNet test (Join-Path $repoRoot 'tests\PSWindowsUpdateGui.Tests\PSWindowsUpdateGui.Tests.csproj') -c $Configuration '-p:Platform=x64' --logger 'trx;LogFileName=PSWindowsUpdateGui.Tests.trx'
& (Join-Path $PSScriptRoot 'Test-GuiSmoke.ps1') -Configuration $Configuration
Invoke-DotNet test (Join-Path $repoRoot 'tests\PSWindowsUpdateGui.Tests\PSWindowsUpdateGui.Tests.csproj') -c $Configuration '-p:Platform=x64' --no-build --no-restore --logger 'trx;LogFileName=PSWindowsUpdateGui.Tests.trx'
}

$releaseRoot = Join-Path $repoRoot 'artifacts\release'
if (Test-Path -LiteralPath $releaseRoot) {
$resolvedRelease = [System.IO.Path]::GetFullPath($releaseRoot).TrimEnd('\')
$expectedRelease = [System.IO.Path]::GetFullPath((Join-Path $repoRoot 'artifacts\release')).TrimEnd('\')
if ($resolvedRelease -ne $expectedRelease) { throw "Refusing to clean unexpected release path: $resolvedRelease" }
Remove-Item -LiteralPath $resolvedRelease -Recurse -Force
$publishRoot = Join-Path $repoRoot 'artifacts\publish'
foreach ($directory in @($releaseRoot, $publishRoot)) {
if (Test-Path -LiteralPath $directory) {
$resolvedDirectory = [System.IO.Path]::GetFullPath($directory).TrimEnd('\')
$expectedDirectory = [System.IO.Path]::GetFullPath($directory).TrimEnd('\')
if ($resolvedDirectory -ne $expectedDirectory -or -not $resolvedDirectory.StartsWith([System.IO.Path]::GetFullPath((Join-Path $repoRoot 'artifacts')).TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to clean unexpected artifact path: $resolvedDirectory"
}
Remove-Item -LiteralPath $resolvedDirectory -Recurse -Force
}
}
New-Item -ItemType Directory -Path $releaseRoot -Force | Out-Null
$source = Join-Path $repoRoot "src\PSWindowsUpdateGui\bin\x64\$Configuration\net48\PSWindowsUpdateGUI.exe"
New-Item -ItemType Directory -Path $publishRoot -Force | Out-Null

Invoke-DotNet restore (Join-Path $repoRoot 'src\PSWindowsUpdateGui\PSWindowsUpdateGui.csproj') -r win-x64 --locked-mode '-p:Platform=x64' '-p:SelfContained=true'
Invoke-DotNet publish (Join-Path $repoRoot 'src\PSWindowsUpdateGui\PSWindowsUpdateGui.csproj') -c $Configuration -r win-x64 --self-contained true --no-restore '-p:Platform=x64' "-p:Version=$Version" '-p:WindowsPackageType=None' '-p:WindowsAppSDKSelfContained=true' '-p:PublishSingleFile=true' '-p:IncludeAllContentForSelfExtract=true' '-p:EnableMsixTooling=true' '-p:PublishTrimmed=false' '-p:PublishReadyToRun=false' '-o' $publishRoot

$source = Join-Path $publishRoot 'PSWindowsUpdateGUI.exe'
if (-not (Test-Path -LiteralPath $source)) { throw "Single-file publish did not create $source." }
& (Join-Path $PSScriptRoot 'Verify-Release.ps1') -Executable $source
$destination = Join-Path $releaseRoot 'PSWindowsUpdateGUI.exe'
Copy-Item -LiteralPath $source -Destination $destination -Force
$checksum = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() + ' PSWindowsUpdateGUI.exe'
Set-Content -LiteralPath (Join-Path $releaseRoot 'PSWindowsUpdateGUI.exe.sha256') -Value $checksum -Encoding ASCII
Copy-Item -LiteralPath (Join-Path $repoRoot 'THIRD-PARTY-NOTICES.txt') -Destination $releaseRoot -Force
& (Join-Path $PSScriptRoot 'New-Sbom.ps1') -Executable $destination -OutputPath (Join-Path $releaseRoot 'PSWindowsUpdateGUI.spdx.json') -Version $Version
if (-not $SkipTests) {
& (Join-Path $PSScriptRoot 'Test-GuiSmoke.ps1') -Configuration $Configuration
}
Write-Host "Release artifacts: $releaseRoot"
48 changes: 30 additions & 18 deletions build/Test-GuiSmoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,41 @@ param(

$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $PSScriptRoot
$executable = Join-Path $repoRoot "tests\PSWindowsUpdateGui.UiSmoke\bin\x64\$Configuration\net48\PSWindowsUpdateGui.UiSmoke.exe"
$dotnet = if ($env:DOTNET_EXE) { $env:DOTNET_EXE } else { 'dotnet' }
$project = Join-Path $repoRoot 'src\PSWindowsUpdateGui\PSWindowsUpdateGui.csproj'
$smokeRoot = Join-Path $repoRoot 'artifacts\ui-smoke-build'
$darkCapture = Join-Path $repoRoot 'artifacts\ui-smoke-history-dark.png'
$lightCapture = Join-Path $repoRoot 'artifacts\ui-smoke-updates-light.png'
$executable = Join-Path $smokeRoot 'PSWindowsUpdateGUI.exe'
$errorLog = Join-Path (Split-Path -Parent $executable) 'ui-smoke-error.log'
if (-not (Test-Path -LiteralPath $executable)) { throw "GUI smoke harness was not built: $executable" }
if (Test-Path -LiteralPath $errorLog) { Remove-Item -LiteralPath $errorLog -Force }
& $dotnet build $project -c UiSmoke '-p:Platform=x64' '-p:UiSmokeBuild=true' "-p:OutputPath=$smokeRoot\" --no-restore

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Locate the smoke executable under the framework/RID output path

This script will fail before running either smoke scenario: passing -p:OutputPath=$smokeRoot\ to an SDK-style build does not suppress the default target-framework and runtime-identifier suffixes, and this project sets both net10.0-windows10.0.26100.0 and win-x64. The executable is therefore emitted below $smokeRoot\net10.0-windows10.0.26100.0\win-x64\, while the next check looks only for $smokeRoot\PSWindowsUpdateGUI.exe; every normal Build.ps1 run with tests enabled reaches that failing check.

Useful? React with 👍 / 👎.

if ($LASTEXITCODE -ne 0) { throw "WinUI smoke build failed with exit code $LASTEXITCODE." }
if (-not (Test-Path -LiteralPath $executable)) { throw "WinUI smoke executable was not built: $executable" }

$process = Start-Process -FilePath $executable -PassThru -WindowStyle Hidden
foreach ($file in @($errorLog, $darkCapture, $lightCapture)) {
if (Test-Path -LiteralPath $file) { Remove-Item -LiteralPath $file -Force }
}

$previousDotnetRootX64 = $env:DOTNET_ROOT_X64
try {
$deadline = [DateTime]::UtcNow.AddSeconds(20)
do {
Start-Sleep -Milliseconds 200
$process.Refresh()
if ($process.HasExited) {
if ($dotnet -ne 'dotnet') { $env:DOTNET_ROOT_X64 = [System.IO.Path]::GetFullPath((Split-Path -Parent $dotnet)) }
$scenarios = @(
@{ Theme = 'Dark'; Page = 'history'; Capture = $darkCapture },
@{ Theme = 'Light'; Page = 'updates'; Capture = $lightCapture }
)
foreach ($scenario in $scenarios) {
$process = Start-Process -FilePath $executable -ArgumentList @('--ui-smoke', '--theme', $scenario.Theme, '--page', $scenario.Page, '--capture', "`"$($scenario.Capture)`"") -PassThru -Wait

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound each smoke scenario with a timeout

If WinUI initialization, activation, or screenshot capture deadlocks, Start-Process -Wait waits indefinitely and leaves CI/build invocations hung rather than reporting a failed smoke test. The previous harness had a 20-second deadline; retain an equivalent timeout and terminate the process so a broken GUI startup fails predictably.

Useful? React with 👍 / 👎.

if ($process.ExitCode -ne 0) {
$detail = if (Test-Path -LiteralPath $errorLog) { Get-Content -LiteralPath $errorLog -Raw } else { "Exit code $($process.ExitCode)." }
throw "GUI smoke harness exited before creating a window. $detail"
throw "WinUI $($scenario.Theme) $($scenario.Page) smoke test failed. $detail"
}
} while ($process.MainWindowHandle -eq [IntPtr]::Zero -and [DateTime]::UtcNow -lt $deadline)

if ($process.MainWindowHandle -eq [IntPtr]::Zero) { throw 'GUI smoke harness did not create a WPF window within 20 seconds.' }
Write-Host "GUI smoke window created successfully (PID $($process.Id))."
}
}
finally {
if (-not $process.HasExited) {
[void] $process.CloseMainWindow()
if (-not $process.WaitForExit(5000)) { Stop-Process -Id $process.Id -Force }
}
$env:DOTNET_ROOT_X64 = $previousDotnetRootX64
}

foreach ($capture in @($darkCapture, $lightCapture)) {
if (-not (Test-Path -LiteralPath $capture) -or (Get-Item -LiteralPath $capture).Length -lt 1024) { throw "WinUI smoke test did not create a valid screenshot: $capture" }
}
Write-Host "WinUI production-assembly smoke tests passed: $darkCapture, $lightCapture"
Loading
Loading