From 0c791d7738ad70d0a03b72a7519cddd9d39f69aa Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 15:09:59 +0200 Subject: [PATCH 01/10] Added new tests to main branch --- run-ui-tests.ps1 | 142 ++++++++++++++++++ .../Infrastructure/NativeCommands.cs | 13 ++ .../Infrastructure/UiTestSettings.cs | 20 +++ tests/FileManager.UiTests/README.md | 29 +++- .../ReportedDefectCharacterizationUiTests.cs | 104 +++++++++++++ .../SChannelTlsIntegrationTests.cs | 84 ++++++++--- 6 files changed, 364 insertions(+), 28 deletions(-) create mode 100644 run-ui-tests.ps1 create mode 100644 tests/FileManager.UiTests/ReportedDefectCharacterizationUiTests.cs diff --git a/run-ui-tests.ps1 b/run-ui-tests.ps1 new file mode 100644 index 0000000..b574211 --- /dev/null +++ b/run-ui-tests.ps1 @@ -0,0 +1,142 @@ +[CmdletBinding()] +param( + [string]$ExecutablePath, + [string]$Filter = 'TestCategory=UI', + [string]$Logger = 'console;verbosity=normal', + [switch]$NoBuild, + [switch]$ListTests, + [switch]$ConfirmIsolatedProfile, + [string[]]$AdditionalDotNetArguments = @() +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repositoryRoot = $PSScriptRoot +$testProject = Join-Path $repositoryRoot 'tests\FileManager.UiTests\FileManager.UiTests.csproj' + +function Resolve-FileManagerExecutable { + param([string]$RequestedPath) + + if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) { + # An explicit selection is authoritative so a typo cannot silently run a different executable. + if (-not (Test-Path -LiteralPath $RequestedPath -PathType Leaf)) { + throw "The requested FileManager executable was not found: $RequestedPath" + } + return (Resolve-Path -LiteralPath $RequestedPath).Path + } + + $candidates = [System.Collections.Generic.List[string]]::new() + if (-not [string]::IsNullOrWhiteSpace($env:FILEMANAGER_UI_EXE)) { + # Preserve an explicit caller selection before checking this checkout's standard build locations. + $candidates.Add($env:FILEMANAGER_UI_EXE) + } + foreach ($relativePath in @( + 'src\vcxproj\salamander\Debug_x64\salamand.exe', + 'src\vcxproj\salamander\Release_x64\salamand.exe', + 'src\vcxproj\salamander\Debug_x86\salamand.exe', + 'src\vcxproj\salamander\Release_x86\salamand.exe' + )) { + # Checkout-relative discovery remains portable across users, drive letters, and repository locations. + $candidates.Add((Join-Path $repositoryRoot $relativePath)) + } + + foreach ($candidate in $candidates) { + if (-not [string]::IsNullOrWhiteSpace($candidate) -and + (Test-Path -LiteralPath $candidate -PathType Leaf)) { + return (Resolve-Path -LiteralPath $candidate).Path + } + } + + # Do not silently fall back to an installed copy: a stale executable makes every native-command test fail misleadingly. + throw 'No checkout salamand.exe was found. Build the current branch or pass -ExecutablePath explicitly.' +} + +if (-not $ConfirmIsolatedProfile) { + # Explicit acknowledgement keeps interactive UI work out of a normal unattended desktop session. + throw 'Re-run with -ConfirmIsolatedProfile to use the guarded filesystem and registry test sandbox.' +} +if (-not (Test-Path -LiteralPath $testProject -PathType Leaf)) { + throw "The UI test project was not found: $testProject" +} +if ($null -eq (Get-Command dotnet -ErrorAction SilentlyContinue)) { + throw 'dotnet was not found on PATH.' +} + +$resolvedExecutable = Resolve-FileManagerExecutable $ExecutablePath +$executableDirectory = Split-Path -Parent $resolvedExecutable +$crashReporter = Join-Path $executableDirectory 'salmon.exe' +# Fail once in preflight instead of letting every UI case stall on the application's missing-reporter dialog. +if (-not (Test-Path -LiteralPath $crashReporter -PathType Leaf)) { + throw "The selected test artifact is incomplete; salmon.exe is missing beside salamand.exe: $crashReporter" +} + +$sandboxParent = Join-Path ([IO.Path]::GetTempPath()) ('OpenSalamanderUiTests-' + [Guid]::NewGuid().ToString('N')) +$testDataRoot = Join-Path $sandboxParent 'filemanager-testdata' +$configurationRoot = 'Software\Open Salamander\6.0-filemanager-testdata' +$savedEnvironment = @{ + FILEMANAGER_UI_ISOLATED = $env:FILEMANAGER_UI_ISOLATED + FILEMANAGER_UI_EXE = $env:FILEMANAGER_UI_EXE + FILEMANAGER_UI_TESTDATA_ROOT = $env:FILEMANAGER_UI_TESTDATA_ROOT + FILEMANAGER_UI_CONFIG_ROOT = $env:FILEMANAGER_UI_CONFIG_ROOT +} +$testExitCode = 1 + +try { + $env:FILEMANAGER_UI_ISOLATED = '1' + $env:FILEMANAGER_UI_EXE = $resolvedExecutable + $env:FILEMANAGER_UI_TESTDATA_ROOT = $testDataRoot + $env:FILEMANAGER_UI_CONFIG_ROOT = $configurationRoot + + $arguments = [System.Collections.Generic.List[string]]::new() + $arguments.Add('test') + $arguments.Add($testProject) + if ($NoBuild) { + # A no-build rerun also avoids restore, which would regenerate ignored NuGet intermediates. + $arguments.Add('--no-build') + $arguments.Add('--no-restore') + } + if ($ListTests) { + $arguments.Add('--list-tests') + } + if (-not [string]::IsNullOrWhiteSpace($Filter)) { + $arguments.Add('--filter') + $arguments.Add($Filter) + } + if (-not [string]::IsNullOrWhiteSpace($Logger)) { + $arguments.Add('--logger') + $arguments.Add($Logger) + } + foreach ($argument in $AdditionalDotNetArguments) { + $arguments.Add($argument) + } + + Write-Host 'Running FileManager UI tests' + Write-Host " Project: $testProject" + Write-Host " Executable: $resolvedExecutable" + Write-Host " Sandbox: $testDataRoot" + Write-Host " Filter: $Filter" + + Push-Location $repositoryRoot + try { + & dotnet @($arguments.ToArray()) + $testExitCode = $LASTEXITCODE + } + finally { + Pop-Location + } +} +finally { + # Restore the caller's shell exactly so focused reruns cannot contaminate later commands. + foreach ($name in $savedEnvironment.Keys) { + Set-Item -Path "Env:$name" -Value $savedEnvironment[$name] + } + + # NUnit removes the marked child on a healthy run; remove only the unique empty parent created above. + if ((Test-Path -LiteralPath $sandboxParent -PathType Container) -and + @(Get-ChildItem -LiteralPath $sandboxParent -Force).Count -eq 0) { + Remove-Item -LiteralPath $sandboxParent + } +} + +exit $testExitCode diff --git a/tests/FileManager.UiTests/Infrastructure/NativeCommands.cs b/tests/FileManager.UiTests/Infrastructure/NativeCommands.cs index 2701ff0..f483288 100644 --- a/tests/FileManager.UiTests/Infrastructure/NativeCommands.cs +++ b/tests/FileManager.UiTests/Infrastructure/NativeCommands.cs @@ -13,10 +13,14 @@ internal static class NativeCommands internal const int MoveFiles = 728; internal const int DeleteFiles = 729; internal const int CreateDirectory = 730; + // CM_OPEN drives the same archive-opening path as the Files > Open action without localized menu lookup. + internal const int OpenFile = 732; // These stable host commands cover the three distinct file-discovery and file-opening paths. internal const int FindFiles = 741; internal const int ViewFile = 742; internal const int EditFile = 743; + // CM_HELP_SEARCH opens the HTML Help search tab while keeping the test independent of translated menu labels. + internal const int HelpSearch = 2212; internal const int RenameFile = 754; // CM_ACTIVEREFRESH synchronously refreshes the active file panel before a test quick-searches newly created files. internal const int RefreshActivePanel = 740; @@ -39,6 +43,8 @@ internal static class NativeCommands private const int ConfigurationClearReadOnlyCheckBox = 304; internal const int OperationPathControl = 210; private const int VkEscape = 0x1B; + // HTML Help recognizes VK_RETURN from its search edit even when UIA exposes no reliable submit action. + private const int VkReturn = 0x0D; private const uint WmSetText = 0x000C; private const uint WmGetText = 0x000D; private const uint WmGetTextLength = 0x000E; @@ -524,6 +530,13 @@ internal static void QuickSearch(nint listHandle, string name) SendMessage(listHandle, WmChar, character, 1); } + internal static void PressEnter(nint controlHandle) + { + // HTML Help commits its search field through the native Enter key path rather than a reliable UIA action. + SetFocus(controlHandle); + SendMessage(controlHandle, WmKeyDown, VkReturn, 0); + } + internal static void ToggleFocusedSelection(nint listHandle) { // Insert is the native panel gesture that selects the focused item while preserving prior selections. diff --git a/tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs b/tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs index 3ec5f2a..a79c68a 100644 --- a/tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs +++ b/tests/FileManager.UiTests/Infrastructure/UiTestSettings.cs @@ -50,6 +50,26 @@ internal static void RequireConfigurationFaultInjection() Assert.Ignore("Set FILEMANAGER_UI_CONFIG_FAULT_INJECTION=1 to run the structural configuration write-boundary recovery test."); } + internal static void RequireZipPlugin() + { + RequireTestSandbox(); + // The opt-in confirms deployment and enablement because the public UI cannot distinguish a disabled plug-in from an absent one. + if (!string.Equals(Environment.GetEnvironmentVariable("FILEMANAGER_UI_ZIP_PLUGIN"), "1", StringComparison.Ordinal)) + Assert.Ignore("Install and enable the Zip plug-in, then set FILEMANAGER_UI_ZIP_PLUGIN=1 to run ZIP navigation characterization."); + } + + internal static (string SearchTerm, string ExpectedResult) RequireHelpSearchFixture() + { + RequireTestSandbox(); + var term = Environment.GetEnvironmentVariable("FILEMANAGER_UI_HELP_SEARCH_TERM"); + var expected = Environment.GetEnvironmentVariable("FILEMANAGER_UI_HELP_EXPECTED_RESULT"); + // Language-specific input keeps the contract meaningful for every deployed CHM rather than assuming English result text. + if (string.IsNullOrWhiteSpace(term) || string.IsNullOrWhiteSpace(expected)) + Assert.Ignore("Deploy salamand.chm and set FILEMANAGER_UI_HELP_SEARCH_TERM plus FILEMANAGER_UI_HELP_EXPECTED_RESULT for its language."); + + return (term!, expected!); + } + internal static string RequireCrossVolumeRoot() { var root = Environment.GetEnvironmentVariable("FILEMANAGER_UI_CROSS_VOLUME_ROOT"); diff --git a/tests/FileManager.UiTests/README.md b/tests/FileManager.UiTests/README.md index 858f56e..b0d2d3c 100644 --- a/tests/FileManager.UiTests/README.md +++ b/tests/FileManager.UiTests/README.md @@ -1,10 +1,10 @@ # FileManager UI tests -This project contains 100 basic, parameterized FlaUI/UIA3 NUnit cases plus focused file-operation characterization cases for the native FileManager UI. The cases cover application launch, accessibility-tree discovery, Configuration dialog cancel/commit/restart flows, a committed setting verified after restart, FTP bookmark creation plus edit verified after restart, and native disk create/copy/rename/move/delete/find/view/edit commands. +This project contains parameterized FlaUI/UIA3 lifecycle cases together with focused file-operation, recovery, plug-in, toolbar, TLS, and native-safety characterization tests for the native FileManager UI. The cases cover application launch, accessibility-tree discovery, Configuration dialog cancel/commit/restart flows, a committed setting verified after restart, FTP bookmark creation plus edit verified after restart, and native disk create/copy/rename/move/delete/find/view/edit commands. The tests run as the current interactive Windows user only when a guarded filesystem and registry sandbox are selected. The harness creates and deletes its own `filemanager-testdata` directory and the suffixed registry key after each run. -Set these environment variables before running: +The repository runner configures the guarded test-data root, configuration root, and executable automatically. Set variables manually only when invoking `dotnet test` directly or enabling an optional lane: - `FILEMANAGER_UI_TESTDATA_ROOT` — absolute path ending in `filemanager-testdata`; all test-created files live below it. - `FILEMANAGER_UI_CONFIG_ROOT=Software\Open Salamander\6.0-filemanager-testdata` — selects the registry tree created and removed by the harness. @@ -12,9 +12,12 @@ Set these environment variables before running: - `FILEMANAGER_UI_ARGUMENTS` — optional command-line arguments, for example a test-only `-c` configuration file. - FTP menu IDs are published with the launching process ID below the owned test-data root, so each fixture waits for the exact FileManager instance it controls before issuing a plug-in command. - `FILEMANAGER_UI_CONFIG_FAULT_INJECTION=1` — explicitly enables the exhaustive transactional-configuration crash-recovery lane described below. -- `FILEMANAGER_UI_CROSS_VOLUME_ROOT` — selected automatically as `D:\filemanager-testdata` when fixed writable `D:\` is available. This enables the cross-volume move characterization fixture; the fixture creates and removes only a GUID-named child below this directory. -- `FILEMANAGER_UI_ADS_UNSUPPORTED_TARGET_ROOT` — selected automatically when fixed writable `D:\` uses FAT/FAT32/exFAT. On NTFS `D:\`, the ADS-unsupported scenario is reported as an allowed capability skip. +- `FILEMANAGER_UI_CROSS_VOLUME_ROOT` — selected automatically by `scripts/runtests.ps1` when fixed writable `D:\` is available, or supplied manually for the focused runner. This enables the cross-volume move characterization fixture; the fixture creates and removes only a GUID-named child below this directory. +- `FILEMANAGER_UI_ADS_UNSUPPORTED_TARGET_ROOT` — selected automatically by `scripts/runtests.ps1` when fixed writable `D:\` uses FAT/FAT32/exFAT, or supplied manually for the focused runner. On NTFS, the ADS-unsupported scenario is reported as an allowed capability skip. - `FILEMANAGER_UI_RECYCLE_BIN=1` — explicitly enables the recycle-bin characterization test. It requires the default recycle-bin delete setting in the isolated profile and adds one disposable file to that profile's recycle bin. +- `FILEMANAGER_UI_ZIP_PLUGIN=1` — confirms that the Zip plug-in is deployed and enabled for the reported ZIP-navigation characterization test. +- `FILEMANAGER_UI_HELP_SEARCH_TERM` — a search term known to exist in the language-specific deployed `salamand.chm`. +- `FILEMANAGER_UI_HELP_EXPECTED_RESULT` — result text expected for that Help search term. The test runner never mounts test virtual disks. If fixed writable `D:\` is unavailable, all second-volume-dependent tests are reported as successful, explicit capability skips. @@ -22,18 +25,32 @@ The complete UI suite also requires `SeCreateSymbolicLinkPrivilege` for its disp runner lacks that privilege, it reports the missing privilege and skips the complete UI suite as an allowed environment limitation; the UI tests are not reported as completed. -Run the suite on an interactive Windows desktop session: +The repository runner creates the guarded filesystem and registry sandbox automatically. On an interactive Windows desktop session, its default filter runs the complete UI category: ```powershell -dotnet test tests/FileManager.UiTests/FileManager.UiTests.csproj --filter TestCategory=UI +.\run-ui-tests.ps1 -ConfirmIsolatedProfile ``` +Run every discovered test in the project, including non-UI contract and TLS cases, by clearing the default filter: + +```powershell +.\run-ui-tests.ps1 -ConfirmIsolatedProfile -Filter '' +``` + +The runner derives its paths from the checkout and deliberately does not fall back to an installed application, because a stale executable can turn all native-command cases into misleading failures. Build the current branch first, or pass its artifact explicitly with `-ExecutablePath`. `-NoBuild` skips both managed build and restore for a repeated run; it does not build the native application. + The configuration dialog is opened through its stable native command ID only to avoid locale-dependent menu text. All window discovery, control inspection, focus, dialog lifecycle, and restart assertions use FlaUI/UIA3. File-operation cases create a fresh GUID directory below `filemanager-testdata` for every test, start the left and right panels with `-l`/`-r`, and use the host's stable native command IDs. They verify files and nested directory trees after normal operations, copy and move overwrite/skip conflict choices, rename overwrite-decline and case-only behavior, mixed-selection deletion, continuation after a skipped delete error, file search, internal viewing, configured-editor launch, metadata preservation, cancellation after the worker has started, and destination/name failures. ADS cases create named, empty, large, and edge-named streams; exercise overwrite and retry after a temporarily denied stream; and verify cross-volume preservation or explicit source retention when an ADS-unsupported target reports metadata loss. The recovery fixture seeds an incomplete durable journal with a ready transactional sibling file before launch, then verifies the real startup reconciliation flow. The tests never empty the recycle bin; they only inspect it after moving test-root data to it. The FTP plug-in menu command has no compile-time host command ID: FileManager allocates it while loading plug-ins. Keep the value in the isolated test environment rather than hard-coding it into the test project. The dialog controls themselves are located by their stable plug-in resource IDs and their persistence is asserted through UIA3 after a full application restart. +## Reported-defect characterization + +`ReportedDefectCharacterizationUiTests` records ZIP navigation after its information dialog and language-specific Help Search results without changing application behavior. A failed product assertion is valid evidence that the reported defect remains; a stalled or crashed test host is not an acceptable result. Existing focused cases cover the other two reports: `Move_overwrite_replaces_the_existing_target_and_removes_the_source` verifies move-collision replacement, and `Edit_file_opens_the_selected_file_in_the_configured_editor` verifies the Files > Edit command through a harness-owned editor. + +ZIP is skipped unless `FILEMANAGER_UI_ZIP_PLUGIN=1` explicitly confirms deployment and enablement. Help Search is skipped unless `help//salamand.chm` is deployed beside the selected executable and both Help fixture variables are set. The tests must not install or reconfigure plug-ins, replace help content, or modify application behavior to make an assertion pass. + ## Live MojeRzeczy FTPS UI lane `MojeRzeczyFtpsUiTests` contacts an external server and is deliberately marked `Explicit` and excluded from `scripts/runtests.ps1` and CI. It reads its credentials only from the variables used by `C:\Projects\FtpMojerzeczy`, configures explicit FTPS on port 21 with passive and binary transfer, accepts a hostname-invalid certificate for the disposable session only, dismisses the plug-in's modeless welcome message, then downloads `/skan.txt` into the disposable test-data root. It waits for the worker to release the file and verifies the downloaded size against `C:\Projects\FtpMojerzeczy\skan.txt`. When the Debug build shows an error dialog, the test records its native text under `TestResults\ftp-debug-error-dialogs`, dismisses it, and fails rather than waiting for desktop input. If either credential is absent, it passes without opening an FTP connection and reports that FTP UI tests have not been performed due to missing credentials. diff --git a/tests/FileManager.UiTests/ReportedDefectCharacterizationUiTests.cs b/tests/FileManager.UiTests/ReportedDefectCharacterizationUiTests.cs new file mode 100644 index 0000000..7d08867 --- /dev/null +++ b/tests/FileManager.UiTests/ReportedDefectCharacterizationUiTests.cs @@ -0,0 +1,104 @@ +using System.IO.Compression; +using FileManager.UiTests.Infrastructure; +using FlaUI.Core.AutomationElements; +using NUnit.Framework; + +namespace FileManager.UiTests; + +[TestFixture] +public sealed class ReportedDefectCharacterizationUiTests : FileOperationUiTestBase +{ + private const string ZipName = "zip-open-characterization.zip"; + private const string ZipPayloadName = "zip-open-payload.txt"; + + protected override void SeedWorkspaceBeforeFileManagerStart(FileOperationWorkspace workspace) + { + // Seed the archive before launch so the native panel's initial enumeration contains the reported fixture. + using var archive = ZipFile.Open(workspace.SourcePath(ZipName), ZipArchiveMode.Create); + var entry = archive.CreateEntry(ZipPayloadName); + using var writer = new StreamWriter(entry.Open()); + writer.Write("zip-characterization-content"); + } + + [Test] + public void Zip_open_after_information_dialog_navigates_into_archive() + { + UiTestSettings.RequireZipPlugin(); + + SelectSourceItem(ZipName); + NativeCommands.Execute(MainWindow.Properties.NativeWindowHandle.Value, NativeCommands.OpenFile); + DismissOptionalInformationDialog(); + + WaitForFileSystem( + () => NativeCommands.GetWindowTitle(MainWindow.Properties.NativeWindowHandle.Value) + .Contains(ZipName, StringComparison.OrdinalIgnoreCase), + "After accepting the ZIP-open message, FileManager did not navigate into the selected archive."); + + // Quick search exercises the archive listing after navigation instead of accepting a decorative title change alone. + SelectSourceItem(ZipPayloadName); + } + + [Test] + public void Help_search_returns_the_configured_existing_help_result() + { + var (searchTerm, expectedResult) = UiTestSettings.RequireHelpSearchFixture(); + RequireDeployedMainHelp(); + Window? helpWindow = null; + try + { + NativeCommands.Execute(MainWindow.Properties.NativeWindowHandle.Value, NativeCommands.HelpSearch); + helpWindow = WaitForDesktopWindow(window => + string.Equals(window.Properties.ClassName.ValueOrDefault, "HH Parent", StringComparison.Ordinal), + "The HTML Help Search window did not open."); + + var searchInput = helpWindow.FindAllDescendants() + .FirstOrDefault(element => element.ControlType == FlaUI.Core.Definitions.ControlType.Edit && element.IsEnabled) + ?.AsTextBox(); + Assert.That(searchInput, Is.Not.Null, "The Help Search panel did not expose an enabled search input."); + + searchInput!.Text = searchTerm; + NativeCommands.PressEnter(searchInput.Properties.NativeWindowHandle.Value); + + WaitForFileSystem( + () => helpWindow.FindAllDescendants() + .Any(element => element.Name.Contains(expectedResult, StringComparison.OrdinalIgnoreCase)), + $"Help Search did not return the configured existing result '{expectedResult}' for '{searchTerm}'."); + } + finally + { + // HTML Help runs outside the FileManager process, so close only the window captured for this test. + if (helpWindow is not null && NativeCommands.WindowExists(helpWindow.Properties.NativeWindowHandle.Value)) + { + helpWindow.Close(); + WaitForWindowToClose(helpWindow); + } + } + } + + private void DismissOptionalInformationDialog() + { + var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3); + while (DateTime.UtcNow < deadline) + { + var dialog = NativeCommands.GetTopLevelWindows(Application.ProcessId) + .FirstOrDefault(handle => handle != MainWindow.Properties.NativeWindowHandle.Value && + NativeCommands.HasDialogButton(handle, 1)); + if (dialog != 0) + { + NativeCommands.ClickDialogButton(dialog, 1); // IDOK + return; + } + Thread.Sleep(100); + } + } + + private static void RequireDeployedMainHelp() + { + var executableDirectory = Path.GetDirectoryName(UiTestSettings.ExecutablePath)!; + var helpDirectory = Path.Combine(executableDirectory, "help"); + // Searching cannot be characterized when the selected artifact has no compiled main help archive. + if (!Directory.Exists(helpDirectory) || + !Directory.EnumerateFiles(helpDirectory, "salamand.chm", SearchOption.AllDirectories).Any()) + Assert.Ignore("Deploy help//salamand.chm beside the tested executable to run Help Search characterization."); + } +} diff --git a/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs b/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs index 217a99d..22a7a11 100644 --- a/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs +++ b/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs @@ -12,17 +12,21 @@ namespace FileManager.UiTests; [NonParallelizable] public sealed class SChannelTlsIntegrationTests { + // Bound both peers so an SChannel negotiation failure is reported instead of pinning the test host indefinitely. + private static readonly TimeSpan HandshakeTimeout = TimeSpan.FromSeconds(15); + [TestCase(SslProtocols.Tls12)] [TestCase(SslProtocols.Tls13)] public async Task LocalTlsServerNegotiatesTheRequiredProtocol(SslProtocols protocol) { + using var timeout = new CancellationTokenSource(HandshakeTimeout); using var certificate = CreateCertificate(); using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); - var server = AcceptAndAuthenticateAsync(listener, certificate, protocol); + var server = AcceptAndAuthenticateAsync(listener, certificate, protocol, timeout.Token); using var client = new TcpClient(); - await client.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port); + await client.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port, timeout.Token); using var stream = new SslStream(client.GetStream(), false, (_, _, _, _) => true); try { @@ -31,45 +35,81 @@ await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions TargetHost = "localhost", EnabledSslProtocols = protocol, CertificateRevocationCheckMode = X509RevocationMode.NoCheck - }); + }, timeout.Token); + + Assert.That(stream.SslProtocol, Is.EqualTo(protocol)); + Assert.That(await server.WaitAsync(timeout.Token), Is.EqualTo(protocol)); + } + catch (OperationCanceledException error) when (timeout.IsCancellationRequested) + { + throw new AssertionException($"The local {protocol} SChannel handshake did not complete within {HandshakeTimeout.TotalSeconds:0} seconds.", error); } catch (Exception clientError) { - try { await server; } + try { await server.WaitAsync(timeout.Token); } + catch (OperationCanceledException serverError) when (timeout.IsCancellationRequested) + { + // Preserve the initiating client failure while classifying the peer cancellation as the shared deadline. + throw new AssertionException($"The local {protocol} SChannel handshake did not complete within {HandshakeTimeout.TotalSeconds:0} seconds.", + new AggregateException(clientError, serverError)); + } catch (Exception serverError) { throw new AssertionException($"TLS server failed: {serverError}", clientError); } throw; } - - Assert.That(stream.SslProtocol, Is.EqualTo(protocol)); - Assert.That(await server, Is.EqualTo(protocol)); + finally + { + await StopAndObserveServerAsync(timeout, listener, server); + } } [Test] public async Task SelfSignedServerCertificateIsRejectedWithoutAnExplicitUserException() { + using var timeout = new CancellationTokenSource(HandshakeTimeout); using var certificate = CreateCertificate(); using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); - var server = AcceptAndAuthenticateAsync(listener, certificate, SslProtocols.Tls12); + var server = AcceptAndAuthenticateAsync(listener, certificate, SslProtocols.Tls12, timeout.Token); using var client = new TcpClient(); - await client.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port); + await client.ConnectAsync(IPAddress.Loopback, ((IPEndPoint)listener.LocalEndpoint).Port, timeout.Token); using var stream = new SslStream(client.GetStream(), false); - Assert.That(async () => await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + try { - TargetHost = "localhost", - EnabledSslProtocols = SslProtocols.Tls12, - CertificateRevocationCheckMode = X509RevocationMode.NoCheck - }), Throws.TypeOf()); + Assert.That(async () => await stream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions + { + TargetHost = "localhost", + EnabledSslProtocols = SslProtocols.Tls12, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck + }, timeout.Token), Throws.TypeOf()); + + try { await server.WaitAsync(timeout.Token); } + catch (AuthenticationException) { } + } + catch (OperationCanceledException error) when (timeout.IsCancellationRequested) + { + throw new AssertionException($"The self-signed certificate rejection did not complete within {HandshakeTimeout.TotalSeconds:0} seconds.", error); + } + finally + { + await StopAndObserveServerAsync(timeout, listener, server); + } + } - try { await server; } - catch (AuthenticationException) { } + private static async Task StopAndObserveServerAsync(CancellationTokenSource timeout, TcpListener listener, Task server) + { + // Cancel, close, and observe every peer task so a failed handshake cannot retain a listener or test-host thread. + timeout.Cancel(); + listener.Stop(); + try { await server.WaitAsync(TimeSpan.FromSeconds(1)); } + catch { } } - private static async Task AcceptAndAuthenticateAsync(TcpListener listener, X509Certificate2 certificate, SslProtocols protocol) + private static async Task AcceptAndAuthenticateAsync(TcpListener listener, X509Certificate2 certificate, + SslProtocols protocol, CancellationToken cancellationToken) { - using var server = await listener.AcceptTcpClientAsync(); + using var server = await listener.AcceptTcpClientAsync(cancellationToken); using var stream = new SslStream(server.GetStream(), false); await stream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions { @@ -77,7 +117,7 @@ await stream.AuthenticateAsServerAsync(new SslServerAuthenticationOptions EnabledSslProtocols = protocol, ClientCertificateRequired = false, CertificateRevocationCheckMode = X509RevocationMode.NoCheck - }); + }, cancellationToken); return stream.SslProtocol; } @@ -92,8 +132,8 @@ private static X509Certificate2 CreateCertificate() request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false)); request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); using var generated = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); - // Windows SChannel cannot use an ephemeral private-key handle for a - // server credential, so use the .NET 10-compatible PFX loader with UserKeySet. - return X509CertificateLoader.LoadPkcs12(generated.Export(X509ContentType.Pfx), null, X509KeyStorageFlags.UserKeySet, null); + // SChannel requires a persisted server key; machine-key import also avoids missing user-key directories in isolated CI profiles. + return X509CertificateLoader.LoadPkcs12(generated.Export(X509ContentType.Pfx), null, + X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable, null); } } From 3a100a406656614230ee13a517f7da107b80e426 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 15:50:05 +0200 Subject: [PATCH 02/10] Resolved problems occured in build --- .github/workflows/pr-msbuild.yml | 19 ++++++++++++++++--- src/fileswindow_navigation.cpp | 9 +++++++-- tools/setup-vs2026-buildtools.ps1 | 11 +++++++---- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 1bbcb2d..affe63d 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -54,9 +54,21 @@ jobs: - name: Setup MSBuild uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2 - - name: Setup MSVC Developer Command Prompt (x64 toolchain) - # Establish the VS 2026 environment before invoking every native build. - run: .\tools\setup-vs2026-buildtools.ps1 + - name: Setup MSVC Developer Command Prompt (${{ matrix.platform }} target) + run: | + # VsDevCmd controls target CRT/LIB paths; PreferredToolArchitecture below controls only the compiler host. + $targetArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + .\tools\setup-vs2026-buildtools.ps1 -TargetArchitecture $targetArchitecture + + - name: Verify MSVC target environment + run: | + # Fail before compilation if a persisted runner environment exposes the wrong target libraries. + $expectedArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedArchitecture) { + throw "Expected MSVC target '$expectedArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." + } + Write-Host "MSVC target architecture: $env:VSCMD_ARG_TGT_ARCH" + Write-Host "MSVC host architecture: $env:VSCMD_ARG_HOST_ARCH" # Run the verifier's synthetic Git-history cases once per matrix to keep # vendor scoping, comment parsing, and actionable diagnostics stable. @@ -133,6 +145,7 @@ jobs: "/p:Configuration=$($env:BUILD_CONFIGURATION)" "/p:Platform=${{ matrix.platform }}" "/p:PlatformToolset=$($env:PLATFORM_TOOLSET)" + # This selects the x64-hosted tools; the target and its CRT paths come from Platform plus VsDevCmd -arch. '/p:PreferredToolArchitecture=x64' '/warnaserror' '/nr:false' diff --git a/src/fileswindow_navigation.cpp b/src/fileswindow_navigation.cpp index 536d5f6..36ec818 100644 --- a/src/fileswindow_navigation.cpp +++ b/src/fileswindow_navigation.cpp @@ -2001,7 +2001,10 @@ BOOL IsWin64RedirectedDirAux(const char* subDir, const char* redirectedDir, cons BOOL IsWin64RedirectedDir(const char* path, char** lastSubDir, BOOL failIfDirWithSameNameExists) { CWidePath pathW(path); - return IsWin64RedirectedDirW(pathW.CStr(), NULL, failIfDirWithSameNameExists); + // Redirection comparisons need the original spelling; reject failed UTF-16 conversion instead of using a removed ambiguous accessor. + if (!pathW.IsValid()) + return FALSE; + return IsWin64RedirectedDirW(pathW.GetDisplayPath(), NULL, failIfDirWithSameNameExists); } BOOL ContainsWin64RedirectedDir(CFilesWindow* panel, int* indexes, int count, char* redirectedDir, BOOL onlyAdded) @@ -2020,7 +2023,9 @@ BOOL ContainsWin64RedirectedDir(CFilesWindow* panel, int* indexes, int count, ch { CPathW fullPathW(pathW); CWidePath dirNameW(dir->Name); - fullPathW.Append(dirNameW.CStr()); + // Preserve the display spelling while composing the path and stop safely if conversion or allocation fails. + if (!dirNameW.IsValid() || !fullPathW.Append(dirNameW.GetDisplayPath())) + continue; if (IsWin64RedirectedDirW(fullPathW.CStr(), NULL, onlyAdded)) { if (FAILED(StringCchCopyA(redirectedDir, MAX_PATH, dir->Name))) diff --git a/tools/setup-vs2026-buildtools.ps1 b/tools/setup-vs2026-buildtools.ps1 index fae3825..e1e0a33 100644 --- a/tools/setup-vs2026-buildtools.ps1 +++ b/tools/setup-vs2026-buildtools.ps1 @@ -1,7 +1,10 @@ # Configure the self-hosted runner from the verified VS 2026 Build Tools path because # the third-party action's version range excludes the installed 18.9 release. [CmdletBinding()] -param() +param( + [ValidateSet('x86', 'x64')] + [string]$TargetArchitecture = 'x64' +) $ErrorActionPreference = 'Stop' @@ -19,8 +22,8 @@ if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { $initialEnvironment = @{} Get-ChildItem Env: | ForEach-Object { $initialEnvironment[$_.Name] = $_.Value } -# Capture the standard x64 developer environment; the x86 MASM path is added below for the mixed-architecture solution. -$command = 'call "' + $vsDevCmd + '" -arch=x64 -host_arch=x64 >nul && set' +# Select target libraries for the matrix platform while retaining the faster x64-hosted compiler for both architectures. +$command = 'call "' + $vsDevCmd + '" -arch=' + $TargetArchitecture + ' -host_arch=x64 >nul && set' $developerEnvironment = & $env:ComSpec /d /s /c $command if ($LASTEXITCODE -ne 0) { throw "VsDevCmd failed with exit code $LASTEXITCODE." @@ -34,7 +37,7 @@ $masm = Get-ChildItem -LiteralPath (Join-Path $visualStudioRoot 'VC\Tools\MSVC') if ($null -eq $masm) { throw "The VS 2026 x86 MASM assembler was not found below '$visualStudioRoot'." } -# Preserve the x64 developer environment while adding the x86 MASM directory used by sfx7zip. +# Preserve the target-specific developer environment while adding the x86 MASM directory used by sfx7zip. $developerEnvironment = @($developerEnvironment | ForEach-Object { if ($_ -like 'Path=*') { 'Path=' + $masm.DirectoryName + ';' + $_.Substring(5) From dc64208ed8b8ce1fc832e9836855159df18549e9 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 16:07:19 +0200 Subject: [PATCH 03/10] Fixed error --- .github/workflows/pr-msbuild.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index affe63d..1f4b7b9 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -137,7 +137,10 @@ jobs: $txtLog = Join-Path $logDir "msbuild-$($env:BUILD_CONFIGURATION)-$suffix.log" Add-Content -Path $env:GITHUB_ENV -Value "LOG_PATH=$txtLog" - + + # Match the tool host to the matrix row as a defense against runner-specific x64 library-path leakage. + $preferredToolArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + $arguments = @( $env:SOLUTION_PATH '/m' @@ -145,8 +148,7 @@ jobs: "/p:Configuration=$($env:BUILD_CONFIGURATION)" "/p:Platform=${{ matrix.platform }}" "/p:PlatformToolset=$($env:PLATFORM_TOOLSET)" - # This selects the x64-hosted tools; the target and its CRT paths come from Platform plus VsDevCmd -arch. - '/p:PreferredToolArchitecture=x64' + "/p:PreferredToolArchitecture=$preferredToolArchitecture" '/warnaserror' '/nr:false' '/v:m' From eb193eda89207292cd02c6506e471b5c803cd651 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 16:24:08 +0200 Subject: [PATCH 04/10] Fixed error v2 --- .github/workflows/pr-msbuild.yml | 9 +++++++++ tools/setup-vs2026-buildtools.ps1 | 10 +--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 1f4b7b9..7022a0d 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -67,6 +67,15 @@ jobs: if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedArchitecture) { throw "Expected MSVC target '$expectedArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." } + # Check executable resolution as well as metadata so an unrelated PATH entry cannot select the opposite-architecture cross tools. + $expectedToolSuffix = "\bin\Hostx64\$expectedArchitecture" + foreach ($toolName in @('cl.exe', 'link.exe')) { + $toolPath = (Get-Command $toolName -CommandType Application -ErrorAction Stop).Source + if ($toolPath.IndexOf($expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { + throw "Expected $toolName below '$expectedToolSuffix', but PATH resolved '$toolPath'." + } + Write-Host "$toolName path: $toolPath" + } Write-Host "MSVC target architecture: $env:VSCMD_ARG_TGT_ARCH" Write-Host "MSVC host architecture: $env:VSCMD_ARG_HOST_ARCH" diff --git a/tools/setup-vs2026-buildtools.ps1 b/tools/setup-vs2026-buildtools.ps1 index e1e0a33..c368e88 100644 --- a/tools/setup-vs2026-buildtools.ps1 +++ b/tools/setup-vs2026-buildtools.ps1 @@ -37,15 +37,7 @@ $masm = Get-ChildItem -LiteralPath (Join-Path $visualStudioRoot 'VC\Tools\MSVC') if ($null -eq $masm) { throw "The VS 2026 x86 MASM assembler was not found below '$visualStudioRoot'." } -# Preserve the target-specific developer environment while adding the x86 MASM directory used by sfx7zip. -$developerEnvironment = @($developerEnvironment | ForEach-Object { - if ($_ -like 'Path=*') { - 'Path=' + $masm.DirectoryName + ';' + $_.Substring(5) - } - else { - $_ - } -}) +# MSBuild locates this assembler through its MASM tool path; exporting its directory would also shadow cl.exe and link.exe with x86 tools. foreach ($line in $developerEnvironment) { $separator = $line.IndexOf('=') From da7cca2118619fcecfb5c8a5ea7676fb06073985 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 17:06:20 +0200 Subject: [PATCH 05/10] Fixed other error --- .github/workflows/pr-msbuild.yml | 23 ++++++++++++++--------- tools/setup-vs2026-buildtools.ps1 | 12 ++++++++---- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 7022a0d..78ebcd1 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -56,22 +56,27 @@ jobs: - name: Setup MSVC Developer Command Prompt (${{ matrix.platform }} target) run: | - # VsDevCmd controls target CRT/LIB paths; PreferredToolArchitecture below controls only the compiler host. - $targetArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } - .\tools\setup-vs2026-buildtools.ps1 -TargetArchitecture $targetArchitecture + # Align host tools and target libraries with the matrix row to avoid inheriting a mixed runner environment. + $architecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + .\tools\setup-vs2026-buildtools.ps1 -TargetArchitecture $architecture -HostArchitecture $architecture - name: Verify MSVC target environment run: | # Fail before compilation if a persisted runner environment exposes the wrong target libraries. - $expectedArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } - if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedArchitecture) { - throw "Expected MSVC target '$expectedArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." + $expectedTargetArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + $expectedHostArchitecture = $expectedTargetArchitecture + if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedTargetArchitecture) { + throw "Expected MSVC target '$expectedTargetArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." + } + if ($env:VSCMD_ARG_HOST_ARCH -ne $expectedHostArchitecture) { + throw "Expected MSVC host '$expectedHostArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_HOST_ARCH'." } # Check executable resolution as well as metadata so an unrelated PATH entry cannot select the opposite-architecture cross tools. - $expectedToolSuffix = "\bin\Hostx64\$expectedArchitecture" + $expectedToolSuffix = "\bin\Host$expectedHostArchitecture\$expectedTargetArchitecture" foreach ($toolName in @('cl.exe', 'link.exe')) { - $toolPath = (Get-Command $toolName -CommandType Application -ErrorAction Stop).Source - if ($toolPath.IndexOf($expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { + $toolCommand = Get-Command $toolName -CommandType Application -ErrorAction Stop | Select-Object -First 1 + [string]$toolPath = $toolCommand.Source + if ($toolPath.IndexOf([string]$expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { throw "Expected $toolName below '$expectedToolSuffix', but PATH resolved '$toolPath'." } Write-Host "$toolName path: $toolPath" diff --git a/tools/setup-vs2026-buildtools.ps1 b/tools/setup-vs2026-buildtools.ps1 index c368e88..158f4d2 100644 --- a/tools/setup-vs2026-buildtools.ps1 +++ b/tools/setup-vs2026-buildtools.ps1 @@ -3,7 +3,10 @@ [CmdletBinding()] param( [ValidateSet('x86', 'x64')] - [string]$TargetArchitecture = 'x64' + [string]$TargetArchitecture = 'x64', + + [ValidateSet('x86', 'x64')] + [string]$HostArchitecture = 'x64' ) $ErrorActionPreference = 'Stop' @@ -22,16 +25,17 @@ if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { $initialEnvironment = @{} Get-ChildItem Env: | ForEach-Object { $initialEnvironment[$_.Name] = $_.Value } -# Select target libraries for the matrix platform while retaining the faster x64-hosted compiler for both architectures. -$command = 'call "' + $vsDevCmd + '" -arch=' + $TargetArchitecture + ' -host_arch=x64 >nul && set' +# Keep host and target selection explicit so each workflow matrix row receives one internally consistent tool environment. +$command = 'call "' + $vsDevCmd + '" -arch=' + $TargetArchitecture + ' -host_arch=' + $HostArchitecture + ' >nul && set' $developerEnvironment = & $env:ComSpec /d /s /c $command if ($LASTEXITCODE -ne 0) { throw "VsDevCmd failed with exit code $LASTEXITCODE." } $visualStudioRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $vsDevCmd)) +$masmPathSuffix = "\bin\Host$HostArchitecture\x86\ml.exe" $masm = Get-ChildItem -LiteralPath (Join-Path $visualStudioRoot 'VC\Tools\MSVC') -Filter 'ml.exe' -File -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match '\\bin\\Hostx64\\x86\\ml\.exe$' } | + Where-Object { $_.FullName -like "*$masmPathSuffix" } | Sort-Object FullName -Descending | Select-Object -First 1 if ($null -eq $masm) { From f6111522f67ac6cbc63a5b5d5e1b6b2d3c5d4efd Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 17:35:08 +0200 Subject: [PATCH 06/10] Revert "Fixed other error" This reverts commit da7cca2118619fcecfb5c8a5ea7676fb06073985. --- .github/workflows/pr-msbuild.yml | 23 +++++++++-------------- tools/setup-vs2026-buildtools.ps1 | 12 ++++-------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 78ebcd1..7022a0d 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -56,27 +56,22 @@ jobs: - name: Setup MSVC Developer Command Prompt (${{ matrix.platform }} target) run: | - # Align host tools and target libraries with the matrix row to avoid inheriting a mixed runner environment. - $architecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } - .\tools\setup-vs2026-buildtools.ps1 -TargetArchitecture $architecture -HostArchitecture $architecture + # VsDevCmd controls target CRT/LIB paths; PreferredToolArchitecture below controls only the compiler host. + $targetArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + .\tools\setup-vs2026-buildtools.ps1 -TargetArchitecture $targetArchitecture - name: Verify MSVC target environment run: | # Fail before compilation if a persisted runner environment exposes the wrong target libraries. - $expectedTargetArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } - $expectedHostArchitecture = $expectedTargetArchitecture - if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedTargetArchitecture) { - throw "Expected MSVC target '$expectedTargetArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." - } - if ($env:VSCMD_ARG_HOST_ARCH -ne $expectedHostArchitecture) { - throw "Expected MSVC host '$expectedHostArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_HOST_ARCH'." + $expectedArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + if ($env:VSCMD_ARG_TGT_ARCH -ne $expectedArchitecture) { + throw "Expected MSVC target '$expectedArchitecture', but VsDevCmd exported '$env:VSCMD_ARG_TGT_ARCH'." } # Check executable resolution as well as metadata so an unrelated PATH entry cannot select the opposite-architecture cross tools. - $expectedToolSuffix = "\bin\Host$expectedHostArchitecture\$expectedTargetArchitecture" + $expectedToolSuffix = "\bin\Hostx64\$expectedArchitecture" foreach ($toolName in @('cl.exe', 'link.exe')) { - $toolCommand = Get-Command $toolName -CommandType Application -ErrorAction Stop | Select-Object -First 1 - [string]$toolPath = $toolCommand.Source - if ($toolPath.IndexOf([string]$expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { + $toolPath = (Get-Command $toolName -CommandType Application -ErrorAction Stop).Source + if ($toolPath.IndexOf($expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { throw "Expected $toolName below '$expectedToolSuffix', but PATH resolved '$toolPath'." } Write-Host "$toolName path: $toolPath" diff --git a/tools/setup-vs2026-buildtools.ps1 b/tools/setup-vs2026-buildtools.ps1 index 158f4d2..c368e88 100644 --- a/tools/setup-vs2026-buildtools.ps1 +++ b/tools/setup-vs2026-buildtools.ps1 @@ -3,10 +3,7 @@ [CmdletBinding()] param( [ValidateSet('x86', 'x64')] - [string]$TargetArchitecture = 'x64', - - [ValidateSet('x86', 'x64')] - [string]$HostArchitecture = 'x64' + [string]$TargetArchitecture = 'x64' ) $ErrorActionPreference = 'Stop' @@ -25,17 +22,16 @@ if ([string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { $initialEnvironment = @{} Get-ChildItem Env: | ForEach-Object { $initialEnvironment[$_.Name] = $_.Value } -# Keep host and target selection explicit so each workflow matrix row receives one internally consistent tool environment. -$command = 'call "' + $vsDevCmd + '" -arch=' + $TargetArchitecture + ' -host_arch=' + $HostArchitecture + ' >nul && set' +# Select target libraries for the matrix platform while retaining the faster x64-hosted compiler for both architectures. +$command = 'call "' + $vsDevCmd + '" -arch=' + $TargetArchitecture + ' -host_arch=x64 >nul && set' $developerEnvironment = & $env:ComSpec /d /s /c $command if ($LASTEXITCODE -ne 0) { throw "VsDevCmd failed with exit code $LASTEXITCODE." } $visualStudioRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $vsDevCmd)) -$masmPathSuffix = "\bin\Host$HostArchitecture\x86\ml.exe" $masm = Get-ChildItem -LiteralPath (Join-Path $visualStudioRoot 'VC\Tools\MSVC') -Filter 'ml.exe' -File -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -like "*$masmPathSuffix" } | + Where-Object { $_.FullName -match '\\bin\\Hostx64\\x86\\ml\.exe$' } | Sort-Object FullName -Descending | Select-Object -First 1 if ($null -eq $masm) { From 0417a271d5b720a64d20c2ad55b7eb942b4af325 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 18:38:48 +0200 Subject: [PATCH 07/10] Fixed error v3 --- .github/workflows/pr-msbuild.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 7022a0d..9f2b4b8 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -70,8 +70,9 @@ jobs: # Check executable resolution as well as metadata so an unrelated PATH entry cannot select the opposite-architecture cross tools. $expectedToolSuffix = "\bin\Hostx64\$expectedArchitecture" foreach ($toolName in @('cl.exe', 'link.exe')) { - $toolPath = (Get-Command $toolName -CommandType Application -ErrorAction Stop).Source - if ($toolPath.IndexOf($expectedToolSuffix, [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { + # Select and stringify one command so verification remains stable when Get-Command exposes multiple application candidates. + [string]$toolPath = (Get-Command $toolName -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source + if ($toolPath -notlike "*$expectedToolSuffix*") { throw "Expected $toolName below '$expectedToolSuffix', but PATH resolved '$toolPath'." } Write-Host "$toolName path: $toolPath" From 4df114af21803879934293518481c5104dca288b Mon Sep 17 00:00:00 2001 From: Patryk Leszczak <69416889+PatrykLs98@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:55:44 +0200 Subject: [PATCH 08/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- run-ui-tests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/run-ui-tests.ps1 b/run-ui-tests.ps1 index b574211..cbdfe2c 100644 --- a/run-ui-tests.ps1 +++ b/run-ui-tests.ps1 @@ -129,7 +129,13 @@ try { finally { # Restore the caller's shell exactly so focused reruns cannot contaminate later commands. foreach ($name in $savedEnvironment.Keys) { - Set-Item -Path "Env:$name" -Value $savedEnvironment[$name] + $value = $savedEnvironment[$name] + if ($null -eq $value) { + Remove-Item -Path "Env:$name" -ErrorAction SilentlyContinue + } + else { + Set-Item -Path "Env:$name" -Value $value + } } # NUnit removes the marked child on a healthy run; remove only the unique empty parent created above. From ebc0e7bc9cc4e392d945c9459812aa2068d4fdea Mon Sep 17 00:00:00 2001 From: Patryk Leszczak <69416889+PatrykLs98@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:55:58 +0200 Subject: [PATCH 09/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../FileManager.UiTests/SChannelTlsIntegrationTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs b/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs index 22a7a11..ec1389b 100644 --- a/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs +++ b/tests/FileManager.UiTests/SChannelTlsIntegrationTests.cs @@ -132,8 +132,14 @@ private static X509Certificate2 CreateCertificate() request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, false)); request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); using var generated = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); - // SChannel requires a persisted server key; machine-key import also avoids missing user-key directories in isolated CI profiles. + // SChannel requires a persisted server key; prefer the current-user store so the tests do not require machine-key write access. + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (!string.IsNullOrWhiteSpace(appData)) + { + Directory.CreateDirectory(Path.Combine(appData, "Microsoft", "Crypto", "RSA")); + Directory.CreateDirectory(Path.Combine(appData, "Microsoft", "Crypto", "Keys")); + } return X509CertificateLoader.LoadPkcs12(generated.Export(X509ContentType.Pfx), null, - X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable, null); + X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.Exportable, null); } } From b30f01e683d86c241d1dd9077e1021432b429816 Mon Sep 17 00:00:00 2001 From: PatrykLs98 Date: Wed, 26 Aug 2026 19:15:52 +0200 Subject: [PATCH 10/10] Added copilot recomendation --- .github/workflows/pr-msbuild.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-msbuild.yml b/.github/workflows/pr-msbuild.yml index 9f2b4b8..effdcd7 100644 --- a/.github/workflows/pr-msbuild.yml +++ b/.github/workflows/pr-msbuild.yml @@ -148,8 +148,8 @@ jobs: Add-Content -Path $env:GITHUB_ENV -Value "LOG_PATH=$txtLog" - # Match the tool host to the matrix row as a defense against runner-specific x64 library-path leakage. - $preferredToolArchitecture = if ('${{ matrix.platform }}' -eq 'Win32') { 'x86' } else { 'x64' } + # Keep MSBuild on the same x64-hosted tools verified above; Platform and VsDevCmd -arch still select the target libraries. + $preferredToolArchitecture = 'x64' $arguments = @( $env:SOLUTION_PATH