## Summary of the Pull Request Adds a `PowerRename.UITests.Next` suite powered by winappcli and automates all 18 scenarios from #40663. The suite covers PowerRename settings, search and replace behavior, regular expressions, formatting and filtering options, file-list interactions, and both classic and Windows 11 context-menu workflows. The PR also stabilizes shared `UITestAutomation.Next` runner lifetimes and settings restoration, adds automation IDs for the original and renamed counters, and prepares unsigned CI builds for PowerRename shell testing. CI now signs the sparse context-menu MSIX and the runner/Settings IPC companions with a disposable machine-trusted test identity. ## PR Checklist - [x] Closes: #40663 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Not applicable; no new shipped product binaries are added - [x] JSON for signing: Not applicable - [x] WXS for installer: Not applicable - [x] YML for CI pipeline: The new UI-test project is discovered through the existing `*UITest*.csproj` pipeline flow and is registered in `PowerToys.slnx` - [x] YML for signed pipeline: Not applicable - [x] **Documentation updated:** Not applicable; there are no user-facing behavior or documentation changes ## Detailed Description of the Pull Request / Additional comments ### PowerRename UI tests - Adds a 25-case `PowerRename.UITests.Next` executable covering all 18 checklist items from #40663. - Exercises classic context-menu registration on Windows 10 and Windows 11. - Exercises the signed Windows 11 tier-1 context menu, including icon visibility and real invocation with an Explorer selection. - Covers search/replace preview and application, text formatting, file/folder/subfolder inclusion, filename/extension scope, enumeration, case sensitivity, match-all behavior, regular expressions, file timestamps, Boost syntax, MRU autocomplete, persisted values, and file-list selection/filtering. - Preserves the existing legacy tests. ### Test reliability and automation hooks - Reuses one runner/Settings lifetime across the complete PowerRename suite to avoid repeated cold launches on constrained agents. - Retains and restores global settings for the full class lifetime and verifies that both Settings and the runner remain healthy. - Propagates scope and PowerRename cleanup failures instead of silently leaking process or profile state. - Adds `OriginalCount` and `RenamedCount` automation IDs. These are automation-only metadata and do not change the visible UI. - Uses stable preview samples, exact count targeting, authoritative Explorer selection, readable classic-menu inventories, and live UIA visibility for popup items. ### Unsigned CI build support - Extends the existing sparse-package test signer with required Authenticode companion files. - PowerRename jobs sign `PowerToys.exe` and `PowerToys.Settings.exe` with the same disposable machine-trusted test identity used for sparse MSIX packages. - This preserves Release IPC authentication while allowing Settings module-toggle commands to work on unsigned PR builds. - Windows 11 and ARM64 PowerRename jobs require a validly signed `PowerRenameContextMenuPackage.msix` before tests start. ## Validation Steps Performed ### Builds and discovery - `PowerRename.UITests.Next` Debug x64 build: passed - `PowerRename.UITests.Next` Debug ARM64 cross-build: passed - `PowerRenameUI` Release x64 build: passed - `UITestAutomation.Next.UnitTests` Debug x64 build: passed - `UITestAutomation.Next.UnitTests`: 16/16 passed - Microsoft.Testing.Platform discovery: 25 unique PowerRename test cases ### Local VM matrix All runs used the complete unfiltered `TestCategory=PowerRename` suite and restored the standard user's settings file byte-for-byte. | Guest | Profile | Result | |---|---|---:| | Windows 10 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 10 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | | Windows 11 x64 | Default, 4 vCPU / 8 GB | 25/25 | | Windows 11 x64 | Constrained, 1 vCPU / 4 GB | 25/25 | ### Azure DevOps UI Test Automation - Final build: [155646235 / 20260824.1](https://dev.azure.com/microsoft/Dart/_build/results?buildId=155646235) - Source revision: `4496683104aea622b30179909bd6e95a17d5500f` - ARM64: 25/25 passed - Windows 10 x64: 25/25 passed - Windows 11 x64: 25/25 passed - Total: 75/75 passed, with zero failed, skipped, not-executed, or unanalyzed results - ARM64 and x64 Release product builds succeeded and published their normal artifacts - Signing steps verified the PowerRename sparse MSIX where applicable and both IPC companion executables on every PowerRename test job <img width="372" height="314" alt="image" src="https://github.com/user-attachments/assets/bc303673-0325-4f85-8d70-ef93110baf5b" />
12 KiB
Shell extensions & the CI signing constraint
Read this before writing or verifying UI tests for any module with a shell extension (context menu, preview handler, thumbnail provider, drag-drop handler). It is the knowledge that is not obvious from the test framework and caused the most trial-and-error.
The one fact that matters most
CI PR-validation builds are UNSIGNED (codeSign:false). Any test that depends on a
sparse-MSIX-packaged shell extension gets 0% on CI, because the package cannot register
(0x800B0100 TRUST_E_NOSIGNATURE). A test that passes on your machine (where you self-signed or
installed a signed build) can therefore fail 100% on CI.
Preferred fix: sign the MSIX on CI and force-trust it
The workarounds below (driving the classic menu, launching the exe directly) let a test pass on an unsigned build, but they do not exercise the modern Win11 tier-1 context menu — the surface real users see. The faithful fix is to give CI a genuinely signed package plus a test-only trusted root, so registration succeeds and the tests drive the real end-user workflow. This is legitimate because the trust anchor is scoped to the test agent and asserts no security — it just makes Windows treat the CI-built package as sideload-installable, exactly like a developer self-signing locally.
Why signing (not Developer Mode) is required. PowerToys registers each sparse package at
module-enable time with PackageManager.AddPackageByUriAsync (src/common/utils/package.h
RegisterSparsePackage) — a packaged deployment that demands a signature chaining to a trusted
root. Developer Mode / register-by-loose-manifest would only help if the product code called
AddPackage -Register AppxManifest.xml, which it does not. So the only route is: sign the .msix
and trust the signer.
Mechanism (all three steps must happen before the module is enabled):
- Create a self-signed code-signing cert whose subject exactly equals the manifest
Publisher— every PowerToys context-menu package and the CmdPalPowerToysSparse.msixuseCN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US. The private key is generated on the agent and never leaves it; nothing is committed. - Force-trust it via the machine stores: import the public cert into
LocalMachine\RootandLocalMachine\TrustedPeople(a self-signed leaf is its own root, and AppX sideload consults TrustedPeople). These import silently, and the CI test agent is elevated (it installs machine-level), so it can write them. Do not import intoCurrentUser\Root— the user Root store raises a CryptoAPI consent dialog that fails non-interactively (UI is not allowed in this operation), even when elevated.CurrentUser\TrustedPeopleis silent and fine as an extra for per-user deployment; a non-elevated run that cannot writeLocalMachine\Rootcannot establish machine root trust silently. signtool sign /fd SHA256every sparse.msixthe product will register.
A ready-to-use, publisher-aware implementation ships with this skill:
.pipelines/signSparsePackages.ps1. It reads each
package's manifest publisher, mints/reuses+trusts a matching cert, and signs only packages that are
not already validly signed (so real framework packages like VCLibs are left alone). Point it at
whichever tree hosts the packages:
# buildNow (run-in-place) — sign the packages in the downloaded build tree:
.\.pipelines\signSparsePackages.ps1 -PackageRoot "$(Pipeline.Workspace)\$(TestArtifactsName)"
# installed (buildNowSlim / official) — sign after install, before the test enables the module.
# Machine install lands in %ProgramFiles%\PowerToys; per-user install in %LOCALAPPDATA%\PowerToys:
.\.pipelines\signSparsePackages.ps1 `
-PackageRoot "$env:ProgramFiles\PowerToys","$env:LOCALAPPDATA\PowerToys" `
-RequiredPackage 'ImageResizerContextMenuPackage.msix'
# local UI-test VM sideload — sign the deployed runtime:
.\.pipelines\signSparsePackages.ps1 -PackageRoot "C:\PowerToysUiTestRun\PowerToys"
Where it is wired in CI. This runs in .pipelines/v2/templates/job-test-project.yml after the
download/install steps and before Run UI Tests. It recursively searches the run-in-place artifact
and complete machine/per-user install roots. Windows 11/ARM64 Image Resizer, PowerRename, and
all-module jobs pass their context-menu MSIX names through -RequiredPackage, so missing, unsigned,
or untrusted setup fails at the prerequisite instead of surfacing later as a product-test failure.
Jobs that do not exercise either modern context menu keep signing best-effort:
PowerRename jobs also pass PowerToys.exe and PowerToys.Settings.exe through
-RequiredAuthenticodeFile on every platform. Release IPC accepts only a Microsoft-named signer
anchored in LocalMachine Root; unsigned PR binaries otherwise let the Settings toggle change
visually while the runner rejects the command as not-microsoft-signed. The same disposable-agent
test identity satisfies that authentication path without weakening the product policy. The job
records the exact thumbprint in a durable agent-work-folder marker. It processes stale markers before
signing, then removes the certificate from LocalMachine/User trust stores and CurrentUser\My
(including its private key) in an always() cleanup step. Failed cleanup keeps the marker for the
next job, so neither interruption nor agent reuse loses the recovery record.
- pwsh: |
$roots = @(
"$(Pipeline.Workspace)\$(TestArtifactsName)",
"$env:ProgramFiles\PowerToys",
"$env:LOCALAPPDATA\PowerToys")
# Build the platform/module-specific arrays as shown in job-test-project.yml.
$requiredPackages = @('PowerRenameContextMenuPackage.msix')
$requiredAuthenticodeFiles = @('PowerToys.exe', 'PowerToys.Settings.exe')
if ($requiredPackages.Count -gt 0 -or $requiredAuthenticodeFiles.Count -gt 0) {
$signingArguments = @{
PackageRoot = $roots
CertificateMarkerPath = "$(Agent.WorkFolder)\PowerToysUiTestState\SigningCertificates.txt"
}
if ($requiredPackages.Count -gt 0) {
$signingArguments.RequiredPackage = $requiredPackages
}
if ($requiredAuthenticodeFiles.Count -gt 0) {
$signingArguments.RequiredAuthenticodeFile = $requiredAuthenticodeFiles
}
& "$(build.sourcesdirectory)\.pipelines\signSparsePackages.ps1" @signingArguments
} else {
try {
& "$(build.sourcesdirectory)\.pipelines\signSparsePackages.ps1" `
-PackageRoot $roots `
-CertificateMarkerPath "$(Agent.WorkFolder)\PowerToysUiTestState\SigningCertificates.txt"
}
catch { Write-Host "##vso[task.logissue type=warning]Sparse MSIX signing skipped: $($_.Exception.Message)" }
}
displayName: "Sign sparse MSIX packages (test trust)"
Prerequisite: signtool.exe. The script finds it across PATH, any Windows Kits install (all
versions, plus the App Certification Kit). As a fallback it verifies and freshly extracts the exact
repository-pinned Microsoft.Windows.SDK.BuildTools .nupkg from the NuGet cache, or downloads that
same version when absent. It verifies the NuGet author/repository signatures and refuses to run a
signtool.exe without a valid Microsoft Authenticode signature. An agent without the SDK therefore
still works without trusting an unversioned or stale extracted tool. Verified end-to-end in a local Win11
VM — the unsigned package fails Add-AppxPackage / AddPackageByUriAsync with 0x800B0100, and
after signSparsePackages.ps1 signs it and the cert is force-trusted (LocalMachine\Root +
TrustedPeople) the same registration succeeds and the package appears in Get-AppxPackage.
Caveat — CmdPal at install time. The installer's custom action stages/registers
PowerToysSparse.msix during install (installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp),
before any test-time signing step runs. Signing after install still covers every module that
registers at enable time (ImageResizer / PowerRename / FileLocksmith / NewPlus). If CmdPal's own
packaged registration is the thing under test on an installed build, the package must instead be
signed at build time (self-sign in the build stage and publish the public .cer for the test
stage to trust) — the run-in-place buildNow path avoids this because nothing registers until the
test enables it.
With this in place a test can drive the modern surface directly on CI. ModernRegistered() (below)
becomes a portability guard for unsigned environments rather than a reason to avoid the modern menu.
Two shell-extension tiers
| Tier | Mechanism | Signing | Unsigned CI PR build |
|---|---|---|---|
Modern (Win11 tier-1 context menu, IExplorerCommand) |
sparse MSIX package | required | ❌ cannot register |
| Classic ("Show more options", Win10 default menu, most preview/thumbnail handlers) | registry COM (HKCU\Software\Classes\...\SystemFileAssociations\<ext>\ShellEx\...) |
none | ✅ works |
Rule: you have two options on an unsigned CI build. (A, preferred) sign the modern package
and force-trust it (see Preferred fix above) so the test drives the real Win11 tier-1 menu.
(B, fallback) drive the signing-free surface — the classic COM menu, or launch the module exe
directly with the files (the PowerRename pattern; the exe often has a CLI/FilesArgument). If you
take the fallback, only assert the modern/tier-1 surface when the package is actually registered,
so it runs on signed/official/installed builds only:
private static bool ModernRegistered() =>
new Windows.Management.Deployment.PackageManager()
.FindPackagesForUser(string.Empty)
.Any(p => p.Id.Name.Contains("<PackageName>", StringComparison.OrdinalIgnoreCase));
Then: OpenContextMenu(useClassicMenu: !ModernRegistered()), and gate modern assertions behind
if (ModernRegistered()).
Debug vs Release — why local != CI
- The classic registry-COM handler is usually gated
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG), so it is compiled out of local Debug builds and present only in CI Release (NDEBUG). To exercise the classic menu against a local Debug runtime, rebuild the extension DLL withENABLE_REGISTRATIONadded to its<PreprocessorDefinitions>. - The sparse
.msixin build output is unsigned; registering it locally needs a self-signed cert whose subject == the packagePublisherand that cert trusted (admin). CI does neither. - Both handlers typically self-gate on the module's enabled flag at query time (classic
QueryContextMenureturnsE_FAIL, modernGetStatereturnsECS_HIDDEN), so the entry tracks the Settings toggle without re-registration. - Modules register handlers at enable time (runtime), and an already-running Explorer will not
surface a freshly-registered handler until the shell restarts — restart
explorer.exeonce after enabling (see PreviewPane / File Explorer add-ons tests).
Reproduce CI's classic scenario on a local (signed) VM
- Rebuild the extension DLL with
ENABLE_REGISTRATIONand deploy it into the guest runtime (C:\PowerToysUiTestRun\PowerToys\WinUI3Apps\). - Neutralize the sparse package so
enable()cannot register it: rename its.msixandGet-AppxPackage *<Package>* | Remove-AppxPackage -AllUsers. This mirrors CI's unsigned failure. - The
ModernRegistered()detection now returns false → the tests drive the classic menu exactly as CI does. UseInvoke-GuestScript.ps1for the guest-side steps.
Slow-agent / cross-arch robustness
Shell-extension tests are especially prone to slow-agent and ARM64-only races that you often cannot reproduce on a local VM (even constrained to 1 core, see customization.md — you cannot emulate ARM64 on an x64 host, so reason from the failure video/screenshot). The robustness patterns — re-select before every attempt, retryable transient popups, verify fixtures reached disk, and slow-path timeouts — are test design and live with the rest of the Explorer/shell test guidance in ui-tests-migration explorer-shell-tests.md.