Run all commands from the repository root. The test suite has four layers:
- PowerShell source-contract and native compatibility probes.
- NUnit source-contract and local TLS integration tests.
- FlaUI/UIA3 tests that drive the native Windows application.
- Optional fault-injection, filesystem-topology, and cross-volume characterization lanes.
- Loopback FTP, FTPS, and HTTP protocol fixtures for fragmented replies, disconnects, TLS, and stalls.
Visual Studio 2026 and its developer-command environment are the authoritative native build environment. Most scripts support Windows PowerShell 5.1. The SQLite recovery probe is the exception: use 64-bit PowerShell 7.4 or newer (pwsh.exe) for an x64 DLL.
The release-parity runner is the authoritative local equivalent of the GitHub Actions release gate and installer-build jobs. Running it without arguments selects this mode automatically:
.\scripts\runtests.ps1The defaults are -BaseCommit HEAD^, -PlatformToolset v145, -BuildNumber 0, the release NUnit filter, and an automatically generated TRX path. Use -NoReleasePipeline when you intentionally want only the ordinary test inventory.
First inspect the blocking environment without mutating disks, build outputs, or installed tools:
.\scripts\runtests.ps1 -PrerequisiteOnlyThe parity command requires an interactive Windows desktop with complete Git history, VS 2026 v145 C++ tools, Windows PowerShell, and PowerShell 7. It probes the host's fixed D:\ drive; when that drive is absent or not writable, all second-volume-dependent UI tests are reported as successful capability skips. It performs the workflow's staged Debug build and strict artifact resolution, then runs the aggregate test gate. Only after that gate passes does it perform the separate Release build, PE audit, symbol validation, pinned Inno Setup installation, staging, and installer compilation. It does not publish a GitHub release.
scripts\build-installer.ps1 remains available for packaging-only iteration, but it is not a replacement for release-pipeline validation.
Run every automated test whose prerequisites are available on the current machine:
powershell -NoProfile -ExecutionPolicy Bypass `
-File .\scripts\runtests.ps1 -NoReleasePipelinescripts/runtests.ps1 collects all PowerShell/native probes, both x64 and x86 compatibility variants, the built 7-Zip wrapper/oracle corpus gate, the complete NUnit project, and the optional Application Verifier lane. It runs every collected check even after a failure and prints passed, failed, and explicitly skipped checks. The 7-Zip gate requires a 7z.exe-compatible independent oracle; strict release runs fail if it is unavailable. The runner creates only the guarded filemanager-testdata directory and the suffixed current-user configuration key before executing the complete UI project.
Each run removes its own GUID-named TestResults\runtests-build-* directory, including after a failure. Pass -KeepBuildArtifacts only when the isolated native build outputs are needed for diagnosis.
Every NUnit case that actually launches salamand.exe also writes a retained execution transcript below a GUID-named
TestResults\ui-test-transcripts-* directory. Unlike the staged build and filemanager-testdata sandbox, these logs
are kept after successful and failed runs. A run skipped before salamand.exe starts produces no transcript.
-PlatformToolset v145 selects the toolset used for both the built executable and native safety target. CI may supply -NUnitTrxPath to retain the complete executable result inventory as a workflow artifact.
Run the complete local equivalent of the GitHub release gate and installer-build jobs with:
.\scripts\runtests.ps1 -ReleasePipeline -BaseCommit HEAD^ -BuildNumber 0-ReleasePipeline applies the release category filter and unexpected-skip policy, omits the nightly Application Verifier diagnostic, then preflights the cached, SHA-256- and Authenticode-verified Inno Setup compiler in portable current-user mode before building Release|x64. The preflight has a bounded installer timeout and retains a wrapper diagnostic log even when Inno Setup cannot create its own log. It audits Release PE hardening, re-runs the native regression subset, verifies the private symbol index, stages the installer, and compiles it. Provisioning logs are retained below the runner temporary directory for diagnosis. It deliberately does not publish a GitHub release. Pass -KeepBuildArtifacts to retain the isolated Debug and Release build trees after diagnosis.
Unlike the ordinary runner, release-pipeline mode refuses ambient UI roots and uses the same fresh-volume topology as Actions. It tests uncommitted local changes so developers can validate the exact snapshot they intend to push; GitHub then exercises that snapshot once committed. Its generated TestResults\runtests-release-gate-*\runtests-v145.trx is the local counterpart of the uploaded GitHub test artifact.
The runner uses the CI pull-request base for changed-line ratchets or accepts -BaseCommit explicitly; it does not guess from a potentially stale local tracking branch. It discovers an existing Debug or Release x64 SQLite DLL when possible. Supply prerequisites explicitly or require a fully provisioned run with:
.\scripts\runtests.ps1 `
-BaseCommit origin/main `
-SqliteDll .\src\vcxproj\sqlite\salamander\Debug_x64\utils\sqlite.dll `
-FailOnSkippedRun individual checks with:
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\verify-operation-completion-protocol.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\verify-durable-copy-commit.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\test-release-input-pinning.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\test-zlib-compatibility.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\test-bzip2-compatibility.ps1The process-scoped execution-policy bypass does not change the machine-wide policy.
The NUnit project targets .NET 10:
dotnet restore .\tests\FileManager.UiTests\FileManager.UiTests.csproj
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj `
--no-restore `
--logger "console;verbosity=minimal"The project declares <IsTestProject>true</IsTestProject>, so no MSBuild command-line override is needed for discovery. When the normal .NET or NuGet directories are unavailable, redirect generated state before restoring:
$env:DOTNET_CLI_HOME = Join-Path $PWD '.dotnet-cli'
$env:NUGET_PACKAGES = Join-Path $PWD '.nuget-packages'
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
$env:DOTNET_NOLOGO = '1'
dotnet restore .\tests\FileManager.UiTests\FileManager.UiTests.csproj
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj `
--no-restore `
--logger "console;verbosity=minimal"These directories contain generated package and CLI state and must not be committed.
UI tests run against the interactive current user only after selecting the guarded test-data and registry boundaries:
$env:FILEMANAGER_UI_TESTDATA_ROOT = "$env:USERPROFILE\filemanager-testdata"
$env:FILEMANAGER_UI_CONFIG_ROOT = 'Software\Open Salamander\6.0-filemanager-testdata'
$env:FILEMANAGER_UI_EXE = 'C:\path\to\salamand.exe'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj `
--filter 'TestCategory=UI'The harness creates and removes only filemanager-testdata and its subfolders, and creates then removes only HKCU\Software\Open Salamander\6.0-filemanager-testdata. Recycle-bin tests inspect the current user's bin but never empty or remove it.
Optional UI settings:
| Variable | Enables |
|---|---|
FILEMANAGER_UI_ARGUMENTS |
Extra application arguments, such as a test-only -c configuration file. |
FILEMANAGER_UI_TRANSCRIPT_ROOT |
Optional absolute output directory for retained per-test salamand.exe execution transcripts. runtests.ps1 assigns a GUID-named directory under TestResults; direct dotnet test runs default below the NUnit work directory. |
FILEMANAGER_UI_FTP_ORGANIZE_COMMAND |
The runtime command ID for FTP Organize Bookmarks persistence cases. |
FILEMANAGER_UI_FTP_CONNECT_COMMAND |
Optional runtime Connect to FTP Server command ID for protocol UI fixture runs outside runtests.ps1; the runner discovers it from the freshly built menu. |
FILEMANAGER_UI_CONFIG_FAULT_INJECTION=1 |
Exhaustive configuration-write crash recovery. |
FILEMANAGER_UI_CROSS_VOLUME_ROOT |
Cross-volume move tests using only a GUID child below the supplied dedicated root. |
FILEMANAGER_UI_ADS_UNSUPPORTED_TARGET_ROOT |
Metadata-loss behavior on a different FAT/FAT32/exFAT-like volume. |
FILEMANAGER_UI_RECYCLE_BIN=1 |
Recycle-bin deletion using the current user's default recycle-bin setting; the test never empties or removes the bin. |
FILEMANAGER_UI_LEAK_CYCLES |
Lifecycle resource samples (5–200; default 20, nightly 100) for handles, GDI, USER, and private bytes. |
Run a specialized lane by category after setting its required environment:
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=FaultInjection'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=CrossVolume'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=AlternateDataStreams'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=Recovery'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=ReparsePoints'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=RecycleBin'
dotnet test .\tests\FileManager.UiTests\FileManager.UiTests.csproj --filter 'TestCategory=LockStress'This live-server test is Explicit and excluded from normal and release-pipeline runs. It uses the MOJERZEC_USERNAME and MOJERZEC_PASSWORD variables from C:\Projects\FtpMojerzeczy, explicit FTPS on port 21, passive mode, binary transfer, and accepts the server's invalid certificate only in the disposable test profile. Debug error dialogs are logged to TestResults\ftp-debug-error-dialogs, dismissed, and reported as a test failure so the external lane does not require a manual click. 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.
$env:MOJERZEC_USERNAME = 'your-username'
$env:MOJERZEC_PASSWORD = 'your-password'
.\scripts\run-ftp-test.ps1See tests/FileManager.UiTests/README.md for additional UI-lane details.
The cmark-gfm probe invokes cl.exe, so start it through Visual Studio 2026:
$vsDevCmd = 'C:\Program Files\Microsoft Visual Studio\18\Community\Common7\Tools\VsDevCmd.bat'
$command = 'call "' + $vsDevCmd + '" -arch=x64 -host_arch=x64 && ' +
'powershell -NoProfile -ExecutionPolicy Bypass ' +
'-File "' + (Join-Path $PWD 'tools\test-cmark-gfm-hardening.ps1') + '"'
& $env:ComSpec /d /s /c $commandAdjust the edition or installation path when necessary.
Build the Debug x64 SQLite target with Visual Studio 2026, then use 64-bit PowerShell 7.4 or newer:
pwsh -NoProfile -ExecutionPolicy Bypass `
-File .\tools\test-sqlite-recovery.ps1 `
-SqliteDll .\src\vcxproj\sqlite\salamander\Debug_x64\utils\sqlite.dllThe changed-line ratchets require the pull request base commit:
.\tools\verify-no-new-terminatethread.ps1 -BaseCommit origin/main
.\tools\verify-no-new-raw-thread-creation.ps1 -BaseCommit origin/main
.\tools\verify-no-new-gettickcount.ps1 -BaseCommit origin/main
.\tools\verify-no-new-max-path-buffers.ps1 -BaseCommit origin/main
.\tools\verify-no-new-unsafe-string-calls.ps1 -BaseCommit origin/mainThe toolbar icon coverage check has no parameters:
.\tools\verify-fluent-icon-coverage.ps1| Test | Description |
|---|---|
verify-operation-completion-protocol.ps1 |
Verifies that cancellation requests do not destroy worker-owned state, workers publish owned completion records, and the UI resumes and closes operations through the asynchronous completion protocol. |
verify-durable-copy-commit.ps1 |
Checks write-through creation, flush/close/verification ordering, transactional replacement, retry paths, and deterministic filesystem fault boundaries for durable copy commits. |
test-release-input-pinning.ps1 |
Checks that release inputs have a complete lock record, all workflow actions use reviewed immutable commits, and publication consumes the gated immutable installer artifact. |
audit-pe-hardening.ps1 |
Inspects linked Release PE headers for ASLR, DEP/NX, CFG, CET compatibility, and high-entropy VA. The release workflow runs it after the Release build. |
test-unsafe-api-baseline.ps1 |
Compares every repository unsafe API fingerprint with the reviewed generated baseline and rejects new or duplicated unsafe calls. |
test-zlib-compatibility.ps1 |
Compiles the checked-in zlib sources and replays the retained legacy, checksum-error, invalid-deflate, and truncated-stream vectors. Supports -Architecture x64 and x86. |
test-bzip2-compatibility.ps1 |
Compiles the checked-in bzip2 sources and checks golden and legacy streams, truncation rejection, and the malformed fuzz-vector corpus. Supports -Architecture x64 and x86. |
test-cmark-gfm-hardening.ps1 |
Compiles the production Markdown renderer, compares retained snapshots, checks safe link and raw-HTML behavior, exercises extension combinations, and enforces input, nesting, node, and output limits. |
test-7zip-compatibility.ps1 |
Archives the retained corpus through the exact built wrapper/library pair, compares independent-oracle extraction manifests, and rejects named header, payload, and footer corruption regressions. |
test-sqlite-recovery.ps1 |
Exercises the supplied SQLite DLL with WAL/FULL settings, interrupted transactions, committed-row recovery, integrity checks, and controlled b-tree corruption detection. |
DeterministicNetworkFixtureTests |
Runs local HTTP, FTP, and FTPS fixtures in every profile for fragmented replies, controlled disconnect, TLS negotiation, and deadlines; with an isolated profile, it also drives the native FTP quick-connect dialog through its fragmented greeting and login boundary. |
verify-no-new-terminatethread.ps1 |
Rejects newly added native TerminateThread calls while leaving legacy debt to dedicated migrations. |
verify-no-new-raw-thread-creation.ps1 |
Rejects new first-party CreateThread or _beginthreadex calls outside the reviewed thread-owner boundaries. |
verify-no-new-gettickcount.ps1 |
Rejects new wrap-prone GetTickCount calls so timeout and scheduling code uses the 64-bit monotonic clock. |
verify-no-new-max-path-buffers.ps1 |
Rejects newly added native fixed arrays whose bounds contain MAX_PATH unless a documented exemption applies. |
verify-no-new-unsafe-string-calls.ps1 |
Rejects newly added unchecked strcpy, strcat, sprintf, lstrcpy, lstrcat, and wsprintf calls. |
verify-fluent-icon-coverage.ps1 |
Ensures every mapped toolbar command has an SVG, core rows no longer use shell fallbacks, SVG colors stay in the approved palette, and the application icon remains separate. |
The zlib and bzip2 .hex files under tests/ and the cmark-gfm Markdown/HTML snapshots are retained inputs consumed by these probes rather than independent runners.
These tests do not drive the FileManager UI and can run in a normal developer profile.
Product_major_is_6_and_build_components_remain_automatic— keeps native, manifest, configuration, shell-extension, installer, workflow, and README version declarations synchronized at product major 6 while preserving automatic build numbering.
LocalTlsServerNegotiatesTheRequiredProtocol(Tls12)— proves a local SChannel client and server negotiate TLS 1.2.LocalTlsServerNegotiatesTheRequiredProtocol(Tls13)— proves a local SChannel client and server negotiate TLS 1.3 when supported by the host.SelfSignedServerCertificateIsRejectedWithoutAnExplicitUserException— proves default validation rejects an untrusted self-signed server certificate.
Customize_toolbar_exposes_persisted_small_medium_and_large_icon_sizes— checks resource, configuration, validation, persistence, and live-change wiring for 16, 24, and 32 pixel toolbar icons.Toolbar_image_lists_scale_independently_from_menu_images_and_app_icon— keeps toolbar scaling separate from menu image lists and the application icon.Fluent_toolbar_spacing_scales_with_each_configured_icon_size— verifies padding, separators, customization layout, startup, and live refresh use the configured icon geometry.Go_to_hot_path_uses_a_conservative_saved_location_glyph— protects the reviewed saved-folder visual and palette for the hot-path command.High_fidelity_reference_catalog_contains_every_runtime_toolbar_icon— requires the 91-icon SVG reference mirror and the expected 3840×3440 catalog image.
Unchecked_string_calls_are_ratchet_gated_and_external_boundaries_report_capacity_and_encoding_failures— keeps unsafe-string ratchets and checked boundary-error reporting in place.New_fixed_max_path_buffers_are_rejected_by_the_changed_lines_ci_ratchet— keeps the changed-lineMAX_PATHbuffer gate and exemption register wired into CI.Bundled_zlib_is_current_has_retained_compatibility_vectors_and_runs_in_ci— pins the zlib vendor record, compatibility vectors, probe, and CI hook.Bundled_bzip2_uses_the_verified_release_and_replays_archive_parser_regressions— pins the bzip2 vendor record, golden/fuzz vectors, probe, and CI hook.Trust_boundary_text_uses_bounded_owned_storage_and_explicit_capacity_failures— checks bounded storage and explicit failures at native text trust boundaries.External_size_fields_use_checked_arithmetic_before_allocation_or_io— requires checked arithmetic before external sizes reach allocation or I/O.Transactional_copy_results_preserve_phase_error_paths_retryability_and_partial_effects_for_legacy_dialogs— preserves structured copy-phase results and their legacy dialog adapters.File_operation_failures_capture_the_primary_error_before_cleanup_and_offer_copyable_context— protects primary error capture and copyable diagnostic context.Kernel_handle_ownership_is_scoped_and_preserves_legacy_close_failures— verifies scoped kernel handles without hiding legacy close errors.Scoped_native_resources_protect_file_operations_and_plugin_boundaries— checks RAII-style native resource protection at operation and plug-in boundaries.Lock_ordering_has_rank_assertions_timeout_diagnostics_and_a_nightly_verifier_lane— protects ranked locks, timeout diagnostics, and verifier workflow coverage.Thread_owners_keep_worker_lifetime_stop_completion_naming_com_and_exception_policy_together— keeps worker lifecycle responsibilities in the common thread-owner abstraction.Crash_report_compression_uses_the_restricted_loader_owned_worker_and_shutdown_contract— verifies crash compression uses restricted loading, owned work, and safe shutdown.Shared_plugin_thread_queue_uses_owned_cooperative_shutdown_without_forced_termination— protects cooperative shutdown of the shared plug-in queue.Seven_zip_task_dispatch_owns_worker_completion_cancellation_and_progress_subclass_lifetime— checks 7-Zip worker, cancellation, completion, and progress-window ownership.Shutdown_deadlines_report_named_phases_and_preserve_shared_state_until_safe_join— protects named shutdown deadlines and state lifetime through joins.Check_path_workers_use_signaled_work_cancellation_and_deadline_waits— requires event-driven path workers with cancellation and bounded waits.Monotonic_64_bit_timers_cross_the_32_bit_boundary_and_reject_backward_samples— checks timer wraparound and backward-sample handling.Win32_path_boundaries_use_owned_wide_paths_and_extended_length_syntax— protects UTF-16 ownership and extended-length Win32 path handling.Destructive_operations_keep_the_handle_identity_guard— requires identity revalidation before destructive filesystem actions.Reparse_point_policy_never_traverses_or_hydrates_unselected_targets— protects the no-traversal/no-hydration reparse-point policy.Copy_engine_uses_unambiguous_64_bit_file_size_and_seek_wrappers— keeps copy sizes and offsets on explicit 64-bit wrappers.Plugin_readers_preserve_full_file_sizes_and_reject_only_their_explicit_caps— parameterized checks keep plug-in readers from narrowing file sizes outside documented caps.Split_combine_stages_and_verifies_output_before_publishing_it— requires staged, verified, write-through split/combine publication.File_operation_planning_uses_an_immutable_plan_and_narrow_filesystem_adapter— protects immutable plans and the injectable filesystem seam.File_operation_correlation_ids_cross_plan_worker_ui_journal_and_log_boundaries— requires one correlation ID across operation layers.Central_retry_policy_bounds_transient_read_retries_and_blocks_destructive_commits— keeps retry attempts bounded and destructive commits gated.Transactional_copy_and_move_expose_each_durable_phase_to_a_deterministic_fault_adapter— protects fault injection at every copy/move durability phase.Native_destructive_operation_characterization_suite_retains_the_required_scenarios— prevents removal of required copy, move, rename, delete, recovery, ADS, Find, View, and Edit UI scenarios.Crash_report_uploads_use_certificate_validated_https_with_explicit_consent— requires explicit consent and validated HTTPS for report uploads.Dynamic_library_loads_use_restricted_search_paths— checks process and call-site DLL search restrictions.Thumbnail_and_archive_metadata_use_the_restartable_parser_broker— protects brokered parsing for thumbnail and archive metadata.Plugin_entry_scope_and_callback_state_restore_host_state_after_an_unwinding_entry_point— verifies plug-in entry unwinding restores callback and host state.Delete_manager_callback_registration_invalidates_before_window_teardown_and_rejects_stale_generations— protects callback-window invalidation and generation checks.Icon_work_pool_bounds_memory_and_prioritizes_visible_current_generation_work— checks icon queue bounds, prioritization, and stale-generation rejection.Directory_listing_uses_bounded_checkpoints_and_a_retained_metadata_budget— requires bounded listing checkpoints and metadata budgets.Allocation_emergency_is_noninteractive_and_defers_recovery_to_the_ui_thread— protects noninteractive allocation failure handling and UI-thread recovery.Background_producers_bound_non_durable_work_and_report_backpressure— checks bounded queues and backpressure diagnostics.Plugin_callbacks_are_contained_and_the_failing_plugin_is_deferred_for_unload— protects callback exception containment and deferred unload.Configuration_saves_stage_validate_and_atomically_select_a_generation— requires staged, validated, atomic configuration generations.Configuration_profiles_are_schema_versioned_migrated_and_validated_before_loading— protects profile schema migration and validation.Metadata_preservation_contract_records_losses_and_gates_move_source_deletion— requires metadata-loss records and user gating before source deletion.Security_descriptor_copy_uses_the_privilege_aware_preservation_matrix— checks privilege-aware security descriptor preservation.Release_diagnostic_ring_is_bounded_sanitized_and_reported_only_through_the_existing_consent_flow— protects bounded sanitized diagnostics and consent-based reporting.Network_operations_have_phase_deadlines_cancellation_and_failure_classification— requires phase-specific network deadlines, cancellation, and error classification.Ftp_downloads_stage_identity_validate_resume_and_publish_only_after_a_durable_commit— protects staged, resumable, identity-checked FTP publication.Ftp_certificate_exceptions_are_endpoint_bound_expiring_and_pinned— checks endpoint-bound, expiring certificate exceptions and pinning.Bundled_7zip_uses_26_02_and_preserves_upgrade_compatibility_contract— pins the 7-Zip version and upgrade compatibility boundary.Bundled_sqlite_uses_a_verified_current_amalgamation_and_exercises_the_owned_database_recovery_contract— pins SQLite provenance, build options, ownership policy, recovery probe, and CI hook.Bundled_cmark_gfm_uses_the_verified_release_and_the_viewer_rejects_unsafe_or_unbounded_rendering— pins cmark-gfm provenance and the renderer hardening contract.
All fixtures in this section require the guarded FILEMANAGER_UI_TESTDATA_ROOT, FILEMANAGER_UI_CONFIG_ROOT, FILEMANAGER_UI_EXE, and an interactive desktop unless a stricter requirement is stated.
The file-operation lane (FileOperationUiTests, FileAccessUiTests, and every fixture deriving from
FileOperationUiTestBase) drives real destructive commands against a live desktop, so its blast radius was audited
explicitly. The findings below describe what the lane is allowed to touch and how that is enforced in code.
Everything the lane creates, mutates, or deletes is seeded by the harness. FileOperationWorkspace builds a fresh
<test-data root>\<GUID>\source and ...\target pair per test and writes every file and directory tree the cases
act on. No case in the lane names a path that is not derived from Workspace.SourcePath, Workspace.TargetPath,
Workspace.SourceDirectory, or Workspace.TargetDirectory; none reads %TEMP%, %USERPROFILE%, a special folder,
or a literal drive path.
The boundaries are enforced rather than assumed:
| Boundary | Enforcement |
|---|---|
| Test-data root | UiTestSettings.TestDataRoot skips the lane unless the path is absolute and its leaf is literally filemanager-testdata. |
| Deletion safety | UiTestSandbox writes a .filemanager-testdata-owner marker into every root it creates and refuses to reuse or delete a directory that lacks it. |
| Registry | Writes are confined to HKCU\Software\Open Salamander\6.0-filemanager-testdata; the lane never touches the live configuration root. |
| Temporary files | Each launched process gets TEMP and TMP redirected below the test-data root. |
| Processes | Teardown kills only the instances the fixture launched, plus crash reporters matched by full path next to the executable under test. |
| Reparse points | Cleanup deletes a junction or symlink itself and never recurses into its target. |
Individual cases that look like they might reach outside do not:
- Invalid-destination failures.
Copy_or_move_failure_does_not_modify_sourceandCreate_directory_failure_keeps_existing_file_intactsubmit a destination inside the workspace that cannot succeed:blocked-targetis seeded as a file, so creating a child below it fails without leaving the sandbox. - Find.
Find_files_searches_subdirectories_from_the_active_panelsets the search root explicitly toWorkspace.SourceDirectorybefore searching, so recursion cannot escape the workspace. It only reads. - View. The internal viewer opens a seeded file and is closed by the test. It only reads.
- Long paths and Unicode.
Unicode_normalization_surrogate_and_long_path_operations_preserve_distinct_entriesbuilds the deepest path the product accepts, still entirely below the workspace. - Recycle Bin.
Delete_to_recycle_bin_...is opt-in (FILEMANAGER_UI_RECYCLE_BIN=1) andShellRecycleBinexposes onlyGetItemCount. The case leaves one harness-created file in the bin and never empties or restores it. - Second volumes. The runner automatically uses
D:\filemanager-testdatawhen fixed writableD:\is available and different from the primary sandbox. NTFS supports the ordinary cross-volume cases; the ADS-unsupported case runs only on FAT/FAT32/exFAT. Missing or unusableD:\produces an explicit successful skip for every test that depends on a second volume. Any selected root is registered as an owned root so the same marker and cleanup rules apply.
One case did reach outside: Edit_file_opens_the_selected_file_in_the_configured_editor. The product seeds
notepad.exe for *.* (src/mainwnd_init.cpp), and on Windows 11 that resolves to the packaged, tabbed Notepad.
Measured on a Windows 11 host, opening a second file produces another process but still one window, whose caption
becomes the newly opened file. The case matched a desktop window by caption and then closed it, so with Notepad
already open it could close a window holding unsaved documents belonging to whoever was running the tests. The same
launch also pinned the workspace as the editor's working directory, which blocked sandbox cleanup afterwards.
The lane now supplies its own editor:
tests/FileManager.UiTests.EditorStubbuildsSandboxEditor.exe, a GUI-subsystem WinForms app that shows the opened file in a window titledSandboxEditor - <file name>. It is a GUI app deliberately: Windows Terminal is the default console delegate on Windows 11 and shares one tabbed window between launches, which would reproduce the same hazard with a console stub.SandboxEditor.Install()copies it into<test-data root>\editor, so the editor binary itself lives in the disposable sandbox and is removed with it.ConfigurationEditorPage.RewriteSelectedEditorpoints the profile at that copy through the real Configuration dialog. A committed configuration generation is checksum-protected, and the product correctly rejects a hand-edited generation and falls back to the previous profile (SelectCommittedConfigurationGenerationinsrc/mainwnd_config.cpp), so the registry must never be written directly. The page is located by content, not by a translated tree label: only the Editors page carries a command, and the seeded value isnotepad.exewhile every seeded viewer entry is empty.SandboxEditor.WaitForProfileEntrywaits for the committed generation to record the change before the restart, because the product writes its profile after the property sheet closes.- The stub moves its own working directory out of the workspace at startup, and teardown kills any stub still running from the sandbox copy, matched by full image path so an editor belonging to the current user is never affected.
If the stub is missing the case is skipped with an explicit message; it never falls back to the machine editor.
Teardown kills the application, which can interrupt an operation and leave an incomplete journal in the shared
<test-data root>\appdata\Open Salamander\operation-journals folder. The next start then raises the modal
Recover file operations prompt, which owns the main window. WaitForNativeMainWindow deliberately skips a
disabled main window, so a single leaked journal strands every remaining case in the run behind a window that never
becomes usable. FileOperationUiTestBase therefore purges that folder both after the application stops and before
the next start, retrying briefly because an exiting process can hold its journal open for a moment.
OperationRecoveryCharacterizationUiTests seeds a journal on purpose and overrides BeforeFileManagerStarted
without calling base, so its own scenario is unaffected.
For the same reason the harness now acknowledges the Open Salamander Configuration notice at startup: an interrupted profile write makes the product report that it fell back to the last verified profile, through an owner-less message box shown before the main window exists.
The lane takes over the desktop: it opens modal confirmation prompts, drives selection, and launches the editor. Run it on a session that can be left alone, not alongside interactive work.
The harness creates one UTF-8 .log file during [SetUp] for every case that passes the sandbox guard and is about
to launch salamand.exe. It starts before sandbox initialization and closes either from the setup-failure handler
or from [TearDown], so setup, test-body, and teardown failures are all retained. The file is attached to the NUnit result and remains under
FILEMANAGER_UI_TRANSCRIPT_ROOT; runtests.ps1 sets that to a GUID-named TestResults\ui-test-transcripts-*
directory. Direct IDE or dotnet test runs use <NUnit work directory>\TestResults\ui-test-transcripts unless the
variable is set explicitly. A prerequisite or category skip that never attempts to launch the executable does not
create an empty log.
Each record has a monotonically increasing sequence number, elapsed seconds from test setup, an absolute UTC timestamp, a category, and details. Read from the header downward to reconstruct the test in order. The most useful categories are:
HARNESS,SANDBOX, andNUNIT: test identity, executable and arguments, sandbox boundaries, restarts, and the NUnit outcome, failure message, and stack trace visible when teardown begins.PROCESS: launch, PID, observation, deliberate termination, and exit ofsalamand.exeand its siblingsalmon.execrash reporter.ACTIONandWAIT: user-equivalent native commands, quick searches, control/button operations, and the start and completion of window waits. Dynamic command IDs remain numeric when no stable host name exists.WINDOW: an opened, changed, closed, or final-snapshot top-level HWND. Dialog and crash-reporter records include their title plus a textual control tree (control ID, class, visible/enabled state, and text). The large volatile main-panel child tree is intentionally omitted. Password-style edit controls are written as<redacted-password>.PRODUCT-DIALOGS: the native product transcript tailed into the retained log while the test runs, followed by a final line-count/path record before the disposable sandbox is deleted. This is authoritative for product dialogs too short-lived to meet the 100 ms HWND observer.CRASH-REPORT,CRASH-REPORT-TEXT, andOBSERVER-ERROR: retained crash-artifact metadata and bounded textual.TXT,.INF,.OPS, or.BUGcontent, plus any diagnostic observer failure. Binary.DMPand.7Zfiles are copied beside the log in a.artifactsdirectory and represented in the log by path, size, and modification time.
For a hang, start at the final ACTION or WAIT, then inspect the following WINDOW and PRODUCT-DIALOGS records.
A product-side SHOW with no matching RESULT identifies an unanswered prompt. For a
crash, find the salamand.exe exit, read the salmon.exe window/control text, then follow the retained artifact
paths. A passing case ends with teardown.complete and transcript.complete; absence of those markers means the
test host itself stopped before normal finalization. Secrets supplied through password edit controls or launch
environment overrides are never logged.
The PRODUCT-DIALOGS section comes from a second, product-side safety net. The lane's hardest failures were all the
same shape: a case timed out waiting for something, and polling alone could not say whether the application had
asked a question, asked a different one, or asked nothing at all. The product writes every dialog it raises while
the sandbox is active:
LogUiTestDialog(src/path_checking.cpp) appends one line per event to<test-data root>\ui-test-dialogs.log, with a timestamp,SHOWorRESULT, the flags, the chosen button, the caption and the text. It is a no-op unlessIsFileManagerUiTestSandboxRequested(), and the file is shared for reading so a run can be watched live.CMessageBox::Execute(src/msgbox.cpp) records every message box, including the ones built directly instead of throughSalMessageBox— the delete confirmation is one of those.- The worker's
WM_USER_DIALOGhandler (src/dialogs_file_ops.cpp) records every operation prompt throughLogUiTestOperationDialog. Those are custom templates rather than message boxes, so nothing else sees them. Only the array slots a given dialog kind really passes as strings are read, because several kinds carry DWORD or BOOL values in the same positions.
A SHOW with no matching RESULT names exactly the prompt a stalled run is waiting on. The harness imports this
file before filemanager-testdata is removed, so it is no longer lost at teardown. That single signal
identified four separate causes: a metadata-preservation gate raised once per item, an unanswered delete
confirmation, the shell's own "File In Use" window appearing instead of the product's error dialog, and an overwrite
prompt proving that a copy had been dispatched for the wrong item.
Both are fixed, and neither was visible without the transcript.
- The Recycle Bin owned the error UI. The worker's recycle-bin delete passed
FOF_SILENT, which hides only the progress dialog. A locked file made the shell raise its own File In Use window, taking the failure out of the operation's Retry/Skip/Skip All contract and blocking the worker on a modal the product does not own.RunRecycleBinDeleteOnStanow also passesFOF_NOERRORUIand reads the per-item result through anIFileOperationProgressSink, becausePerformOperationsreports the batch rather than the item. The recycle-bin branches additionally verify through a handle opened for deletion (VerifyFileDeletable): an attribute-only open bypasses the sharing check entirely, so the previous check accepted a file nobody could delete. ReplaceFileWmerged stale alternate data streams. The transactional copy commits by replacing the destination, andReplaceFileWdeliberately carries the replaced file's streams into the result. A stream that existed only on the old destination therefore outlived the file it belonged to, so an overwrite did not leave the destination equal to the source.RemoveCommittedStreamsMissingFromSourceprunes them, after the commit rather than before it, so a failed commit still leaves the old file untouched.
Neither is fixed, and neither is something the lane should assert away. Both are recorded here because a case that crosses one fails for a reason that has nothing to do with what it is testing.
- Paths stop at
PATH_MAX_PATH. A full directory path is capped at 248 characters including the terminator (src/plugins/shared/spl_gen.h), and the script builder rejects anything longer with Error Building Script. Support for paths beyondMAX_PATHis listed as outstanding inrefactoring.md. The Unicode case therefore computes how many path segments fit inside that budget instead of hard-coding a depth, so it probes the boundary the product actually claims. - Dialog text stops at the ANSI code page. Every dialog is still an ANSI window —
CDialogis only ever constructed withunicodeWndfalse (src/common/winlib.h) — so text placed in an input control is round-tripped through the ANSI code page. A supplementary-plane character reaches the control as?, and the product would rename the file to that. Copying and deleting a name carrying a surrogate pair works and is covered, because those paths never pass the name through a dialog; only the rename target avoids one.
Measured on a Windows 11 developer machine (Release x64 build, v145 toolset). "Before" is the state when this work started, "containment" is after the sandbox and journal work described above, and "after" is current.
| Batch | Before | After containment | After |
|---|---|---|---|
FileOperationUiTests Copy |
2 passed / 11 failed | 7 / 6 | 13 / 0 |
FileOperationUiTests Move + Rename |
2 / 10 | 5 / 7 | 12 / 0 |
FileOperationUiTests Delete, Create, Cancel, Unicode |
7 / 5 | 7 / 5 | 12 / 0 |
FileAccessUiTests |
1 / 2 | 3 / 0 | 3 / 0 |
The lane passes 40 of 40 locally. The first jump came from removing the journal and configuration-notice cascades, which turned one failing case into a run-long series of "did not expose its main window" timeouts. The second came from four harness defects, plus the two product defects above:
- Text never reached the control.
SetOperationPathusedSetWindowText, which across a process boundary updates the cached window title thatGetWindowTextthen reads back. The write and its verification agreed with each other while the control stayed empty and the application read nothing. Copy and Move hid this because their default already names the wanted destination; Create Directory and Rename were submitted with the default instead. Both now go throughWM_SETTEXT/WM_GETTEXT, which repaired five cases at once. - The delete confirmation was never answered.
CMessageBoxassigns its button IDs after the buttons are created, so UI Automation still reports the template placeholder and a search forIDYESfound nothing. The confirmation stood open and the delete silently never ran. It is now found and clicked natively, and the click is posted rather than sent, because answering it runs the whole operation inside the application's message handling. - A gated prompt is raised per item. The metadata-preservation gate that removal of a moved source sits behind
names one item at a time, so answering it once left the rest of the tree waiting.
WaitForFileSystemAnsweringQuestionsanswers while it waits for the outcome. - Fixtures raced the panel. Alternate-data-stream files created inside a case were not listed yet when quick-search ran, so the operation acted on nothing. They are seeded with the rest of the workspace.
Selection deserves its own note. The panel is driven by incremental search, so a fixture whose name is only a prefix of another cannot be addressed on its own: the harness, not the product, would decide which file an operation acted on. The two Unicode normalization fixtures are therefore given distinct ASCII prefixes. They still differ only by the normalization of the same grapheme, which is what that case is about.
A timeout now names the windows that were open, which is what separates "the prompt never appeared" from "a different prompt appeared".
This catalog documents every automated test that drives the native FileManager UI (FlaUI/UIA3), plus the one loopback FTP case that opens the product Connect dialog. Source-contract NUnit cases that never launch salamand.exe remain in NUnit source-contract and integration tests. The four DeterministicNetworkFixtureTests loopback sockets that never open a FileManager window are listed only in the PowerShell/native probe table.
Shared assumptions for every native UI case unless a test states otherwise:
- An interactive current-user Windows desktop, UIA3, and a complete deployed runtime (
salamand.exe,salmon.exe, and the plug-in payloads the selected case needs). - Guarded
FILEMANAGER_UI_TESTDATA_ROOT(leaf namefilemanager-testdata),FILEMANAGER_UI_CONFIG_ROOT, andFILEMANAGER_UI_EXE. - Fixtures are non-parallel; they take over the desktop (modals, selection, optional editor) and must not run beside interactive work.
- File-operation cases start with
-l/-ron a fresh GUID workspace and select items through the panel’s ANSI incremental search, so fixture names must not be prefixes of other listed names. - Dialogs are located by native command IDs and resource/automation IDs, not translated menu text.
- The lane never empties the Recycle Bin, never writes the live configuration hive, and never falls back to the machine’s default editor.
Shared out-of-scope aspects unless a test states otherwise: visual layout and theming, performance/throughput, accessibility beyond UIA3 discovery, plug-in installation, localization of captions, operations outside the sandbox, paths longer than PATH_MAX_PATH, supplementary-plane names typed into ANSI dialogs, and live network except the dedicated FTP cases.
Basic_ui_scenario is one NUnit method parameterized by BasicUiScenarios.All. The seven generated names are categorized UI and LockStress so the nightly verifier lane can repeat this compact matrix independently of the release gate. Prolonged soak belongs to a separately scheduled run, not these seven cases.
- Does: Launches FileManager into the isolated profile and inspects the native main window.
- Tests: Cold-start attachment of UIA3 to the top-level owner window.
- Confirms: The main window has a non-zero HWND, a non-empty title, is enabled, has a non-empty bounding rectangle, and can take focus.
- Assumptions: Shared sandbox and executable; no leftover modal (journal recovery or configuration fallback) owns the main window.
- Out of scope: Panel contents, menus, plug-in load success, and any command other than existing as a usable window.
- Does: Enumerates UIA3 descendants of the main window after launch.
- Tests: That the native UI exposes an automation tree at all.
- Confirms:
FindAllDescendants()returns at least one element. - Assumptions: Shared sandbox; the owner-drawn native menu is not required to appear as a UIA
MenuBar. - Out of scope: Completeness of the tree, names/roles of controls, keyboard navigation, and screen-reader correctness.
- Does: Opens Configuration by command ID, dismisses it without committing, and re-checks the main window.
- Tests: Cancel/close of the configuration property sheet.
- Confirms: The dialog can be opened and cancelled; the main window remains usable afterwards.
- Assumptions: Shared sandbox; Configuration command ID is valid in the deployed build.
- Out of scope: Whether any setting actually changed, persistence, and which page was visible.
- Does: Opens Configuration and accepts it without changing a control, then re-checks the main window.
- Tests: The commit/close lifecycle of the configuration property sheet.
- Confirms: Accepting Configuration does not disable or lose the main window.
- Assumptions: Shared sandbox; a no-op commit is a valid configuration generation.
- Out of scope: Whether the writer produced a new generation, checksum contents, and any visible setting change.
- Does: Toggles the first Configuration checkbox, commits, waits for the asynchronous profile write, restarts FileManager, reopens Configuration, asserts the toggled value, then restores the original value.
- Tests: End-to-end persistence of one committed boolean through process restart.
- Confirms: The first checkbox value after restart matches the committed opposite of the original; the fixture restores independence for later cases.
- Assumptions: The first checkbox is a durable, round-trippable setting; the product finishes writing the profile after the property sheet closes (
WaitForConfigurationClearReadOnlyPersistence). - Out of scope: Other Configuration pages, migration of older schemas, crash-during-write recovery (see
ConfigurationRecoveryUiTests), and UI layout of the sheet.
- Does: Commits Configuration with no intended setting change, restarts FileManager, and re-asserts the main window.
- Tests: Clean process restart after a configuration commit.
- Confirms: A post-commit restart still exposes an enabled, titled, sized main window.
- Assumptions: Shared sandbox; commit does not leave a blocking modal.
- Out of scope: Persistence of a specific setting (covered by
UI_005) and resource-leak budgets (covered byLifecycleLeakUiTests).
- Does: Opens FTP Organize Bookmarks, creates a unique bookmark, renames it, waits for the organizer list and then the isolated profile commit, restarts, and reopens the organizer.
- Tests: FTP plug-in bookmark create/rename persistence across restart.
- Confirms: The edited unique name is present in the organizer both before Close persists the collection and after a full restart.
- Assumptions: The FTP plug-in is deployed;
FILEMANAGER_UI_FTP_ORGANIZE_COMMAND(or the runner-discovered equivalent) identifies Organize Bookmarks; the owner-drawn list is queried natively rather than through UIA item text. - Out of scope: Connecting to a server, transferring files, certificate handling, and bookmark fields other than the displayed name.
- Does: Opens Find Files, sets the mask to
find-target.txt, roots the search atWorkspace.SourceDirectory, enables subdirectory search, disables other options that would change semantics, and clicks Find Now. - Tests: Recursive Find from the workspace source panel using stable control IDs (
2505,2501,2503,2508, results2510). - Confirms: The results list contains exactly one row for the unique nested file.
- Assumptions: The workspace seed includes that unique nested name; Find options are forced so a prior case’s saved Find state cannot change the search.
- Out of scope: Content search, regular expressions, archives, other drives, result activation, and replacing files from Find.
- Does: Selects
view-file.txtand issues View File. - Tests: Dispatch of the internal viewer for a selected text file.
- Confirms: A window of class
Salamander's Viewer Windowopens whose caption contains the file name; the test then closes only that window. - Assumptions: Internal viewer is available; selection via quick-search hits the seeded file.
- Out of scope: Rendered text, encodings, hex view, plugins in the viewer, and printing.
- Does: Rewrites the Editors page in Configuration to
SandboxEditor.exe(copied under the test-data root), waits for the checksum-protected profile, restarts, selectsedit-file.txt, and issues Edit File. - Tests: Files > Edit dispatch to the configured external editor through the real Configuration UI.
- Confirms: A process other than FileManager shows a window titled
SandboxEditor - edit-file.txt. The test closes only that owned window. - Assumptions:
SandboxEditor.exebuilt byFileManager.UiTests.EditorStubis present; Configuration Editors page is identified by content (command field seeded asnotepad.exe); the stub must not be skipped in favor of the machine editor. Windows 11 packaged Notepad is unsafe as an assertion target because it can reuse one tabbed window. - Out of scope: Saving from the editor, associations other than
*.*, DDE, and whatever editor the current user has installed.
All cases below seed a shared workspace (files, trees, ADS, conflict counterparts, blocked-target as a file). Unicode/long-path names are seeded before startup so the panel lists them. Operations wait for destination handles to close before reading content.
- Does: Submits Create Directory with path
created\nestedand answers the intermediate-folderMB_OKCANCELprompt with IDOK. - Tests: Nested directory creation through the native dialog, including the “create the whole branch” confirmation.
- Confirms:
created\nestedexists under the source panel path. - Assumptions: The intermediate folder does not already exist; the affirmative button is IDOK, not IDYES.
- Out of scope: Permissions failures, existing-directory no-ops, and names that do not round-trip through the ANSI dialog.
- Does: Copies
copy-file.txtto the other panel. - Tests: Single-file copy to the opposite panel path.
- Confirms: Target content is
copy-file-content; the source file still exists. - Assumptions: Same-volume workspace; default destination in the copy dialog is the other panel.
- Out of scope: Overwrite, ADS, attributes other than content, and progress UI.
- Does: Sets source last-write time to a fixed UTC timestamp, copies, and compares timestamps after the destination handle is released.
- Tests: Preservation of last-write time on copy.
- Confirms: Target last-write UTC equals the source’s after copy.
- Assumptions: The volume supports the timestamp precision used; metadata is only asserted after the worker closes the destination.
- Out of scope: Creation time, access time, security descriptors, and ADS timestamps.
- Does: Copies
ads-copy.txtafter requiring ADS support on both workspace roots. - Tests: Copy of default data plus named, empty, large, and dotted-name streams.
- Confirms: Default content and each seeded stream (
notes,empty,large,edge name.with.dots) match on the target. - Assumptions: Category
AlternateDataStreams; both panels are on an ADS-capable filesystem (typically NTFS). - Out of scope: Streams the product cannot name, encryption, and sparse-file semantics.
- Does: Copies
ads-overwrite.txtonto a target that already has different default data and astalestream; confirms overwrite with IDYES. - Tests: Confirmed overwrite of ADS: replacement streams appear and streams that existed only on the old target are removed (
ReplaceFileWmust not keep them). - Confirms: Default data becomes
ads-overwrite-source;replacementis present;staleis absent. - Assumptions: ADS support on both volumes; the overwrite prompt is the operation prompt with IDYES.
- Out of scope: Skip/Skip All, and copy to a filesystem that cannot store ADS.
- Does: Holds the source stream
temporarily-deniedopen for read, starts copy, waits for Retry, releases the lock, clicks Retry. - Tests: Retry after a sharing violation on an alternate stream.
- Confirms: After the destination is released, stream
temporarily-deniedcontainsretry-stream-content. - Assumptions: ADS support; the worker surfaces IDRETRY rather than a shell “File In Use” window.
- Out of scope: Permanent denial, Skip of the stream, and default-stream locks.
- Does: Copies
overwrite-file.txtonto an existing different target and confirms IDYES. - Tests: Single-file overwrite confirmation.
- Confirms: Target content becomes
overwrite-source-content; source is unchanged. - Assumptions: A conflict counterpart was seeded in the target panel.
- Out of scope: Overwrite All, timestamps, and ADS.
- Does: Copies
overwrite-all-treeand answers the first conflict with Overwrite All (IDB_ALL/ 185). - Tests: Overwrite All applied to every conflicting descendant without further prompts.
- Confirms:
nested\first.txtandnested\second.txton the target match the source contents. - Assumptions: The tree was seeded with conflicts on those descendants.
- Out of scope: Mix of overwrite and skip, and metadata-loss prompts.
- Does: Copies
skip-file.txtand chooses Skip (IDB_SKIP/ 173). - Tests: Single-file Skip on copy conflict.
- Confirms: Target remains
skip-target-content; source remainsskip-source-content. - Assumptions: Conflict counterpart seeded.
- Out of scope: Skip All and partial tree copy.
- Does: Copies
skip-all-treeand chooses Skip All (IDB_SKIPALL/ 174). - Tests: Skip All for a conflicting directory tree.
- Confirms: Target nested files keep their original target contents; source files still exist.
- Assumptions: Entire nested tree conflicts.
- Out of scope: Nonconflicting siblings (see move Skip All) and source deletion.
- Does: Copies
copy-file.txtand reads the durable.opjjournal under the sandbox journal directory (shared-read, because the worker may still hold it). - Tests: Completed-copy journal shape: plan snapshot, item intent, correlation ID, prepared/committed states, and
OPERATION|completed. - Confirms: A journal naming the source contains
OPERATION|completed, aPLANITEMwith source and target, a correlation ID reused on plan and attempt lines, andSTATEprepared/committed records. - Assumptions: Journals are redirected into the test-data root; completion is appended after planning, so the test waits for the completed record rather than a prefix.
- Out of scope: Crash recovery from this journal (see
OperationRecoveryCharacterizationUiTests), journal encryption, and multi-item plans.
- Does: Copies directory
copy-tree. - Tests: Recursive directory copy.
- Confirms:
copy-tree\nested\payload.txton the target is readable withcopy-tree-content; the source tree remains. - Assumptions: Nested payload was seeded.
- Out of scope: Junctions/symlinks (see
ReparsePointTopologyUiTests) and empty directories as the only coverage.
- Does: Copies two ANSI-round-trippable accented names (
é,ö) selected by distinct ASCII prefixes, copies a deep tree budgeted to 247 usable path characters, then renames the first file and deletes the second. - Tests: Copy/rename/delete of non-ASCII names that survive the process ANSI code page, plus a deep path at the product
PATH_MAX_PATHbudget; file IDs prove copies are new identities. - Confirms: Both copied files and the long-path payload have the expected content and different identities from their sources; rename keeps
first-unicode-contentunder the new accented name; delete removes only the second source name. - Assumptions: Category
Unicode; Western/Central-European ANSI code pages round-tripé/ö; panel quick-search is itself ANSI, so unique ASCII prefixes are required; depth is computed from the sandbox root rather than hard-coded. - Out of scope: NFC vs NFD collapsing, supplementary-plane/surrogate names in dialogs (product gap), and paths longer than
PATH_MAX_PATH.
- Does: Renames
rename-file.txttorenamed-file.txt. - Tests: Simple file rename in the source panel.
- Confirms: New name exists with original content; old name is gone.
- Assumptions: No colliding target name.
- Out of scope: Extension-change associations and undo.
- Does: Renames
rename-treetorenamed-tree. - Tests: Directory rename retaining descendants.
- Confirms: Nested payload exists under the new name with original content; old directory is gone.
- Assumptions: Nested payload seeded.
- Out of scope: Cross-volume rename and junction targets.
- Does: Renames
rename-case.txttoRENAME-CASE.txt. - Tests: Case-only rename on a case-insensitive volume treated as identity, not overwrite.
- Confirms: Directory enumeration shows
RENAME-CASE.txt; content is unchanged. - Assumptions: NTFS-style case-preserving volume; the product does not treat this as a collision.
- Out of scope: Truly case-sensitive filesystems and Explorer display vs on-disk name mismatches elsewhere.
- Does: Sets a known last-write time on
rename-overwrite.txt, renames ontorename-overwrite-target.txt, confirms IDYES. - Tests: Confirmed file-rename overwrite plus timestamp carry-over.
- Confirms: Target content is the source content; source name is gone; last-write UTC matches the prepared timestamp.
- Assumptions: Collision file seeded; overwrite prompt uses IDYES.
- Out of scope: Directory collisions (rejected separately) and ADS on rename.
- Does: Moves
move-file.txtto the other panel after asserting source and target share a volume root. - Tests: Same-volume file move.
- Confirms: Source is gone; target content is
move-file-content. - Assumptions: Default workspace is same-volume (cross-volume is a different fixture).
- Out of scope: Copy-then-delete across volumes and Recycle Bin.
- Does: Moves
move-tree. - Tests: Same-volume recursive move.
- Confirms: Nested payload is at the target with original content; source directory is gone.
- Assumptions: Same-volume workspace.
- Out of scope: Cross-volume metadata gates.
- Does: Moves
move-overwrite.txtonto an existing target and confirms IDYES. - Tests: Confirmed move overwrite: destination replaced before source identity is removed.
- Confirms: Target content is
move-overwrite-source-content; source no longer exists. - Assumptions: Collision seeded on the target panel.
- Out of scope: Skip, Overwrite All, and ADS.
- Does: Moves
move-skip.txtand chooses Skip. - Tests: Skipped move collision.
- Confirms: Target keeps
move-skip-target-content; source keepsmove-skip-source-content. - Assumptions: Collision seeded.
- Out of scope: Partial tree moves.
- Does: Moves
move-overwrite-all-tree, chooses Overwrite All, then answers per-item metadata-preservation questions with IDYES while waiting for the source tree to disappear. - Tests: Overwrite All on move plus the metadata-preservation gate that must complete before source-tree deletion.
- Confirms: Conflicting descendants on the target match source contents; the source tree is eventually gone.
- Assumptions: The metadata gate is raised once per affected item;
WaitForFileSystemAnsweringQuestionsmust keep answering rather than clicking once. - Out of scope: Declining metadata loss (source would remain) and ADS-specific prompts.
- Does: Moves
move-skip-all-treeand chooses Skip All. - Tests: Skip All that still commits independently nonconflicting siblings.
- Confirms: Conflicting target files unchanged; conflicting sources remain;
unique.txtexists only on the target. - Assumptions: Seeded mix of conflicting nested files and one unique sibling.
- Out of scope: Overwrite All and metadata-loss dialogs.
- Does: Selects
delete-file.txt, issues Delete, confirms the delete message box if shown. - Tests: Permanent (or profile-default) deletion of one file from the panel.
- Confirms: The source file no longer exists.
- Assumptions: Isolated profile delete confirmation can be answered via native button click (UIA IDs are placeholders); default may be Recycle Bin unless other tests changed the profile—this case only asserts the path is gone, not the bin.
- Out of scope: Recoverability, locked files, and mixed selection.
- Does: Deletes
delete-treeafter confirmation. - Tests: Recursive directory delete.
- Confirms: The directory tree is gone.
- Assumptions: Confirmation answered; no open handles on descendants.
- Out of scope: Junction delete policy (separate fixture) and Recycle Bin item count.
- Does: Selects
delete-mixed-file.txtanddelete-mixed-treetogether and deletes. - Tests: A delete plan that includes both a file and a recursive directory.
- Confirms: Both the file and the directory tree are gone.
- Assumptions: Multi-select via sequential quick-search and toggle works on the owner-drawn list.
- Out of scope: Partial failure in the mixed set (see locked-file skip).
- Does: Records Recycle Bin item count for the volume, deletes
recycle-file.txt, waits until the source is gone and the count increased. - Tests: Delete-to-bin through the product when Recycle Bin is enabled.
- Confirms: Source path is gone; shell item count for that volume root is higher than before.
- Assumptions: Category
RecycleBin;FILEMANAGER_UI_RECYCLE_BIN=1; the user/profile uses the default Recycle Bin;ShellRecycleBinonly readsGetItemCountand never empties or restores; one harness file is left in the bin. - Out of scope: Restore from the bin, which item appeared, other volumes’ bins, and
FOF_*flags beyond what the product already sets.
- Does: Starts copy of
cancel-conflict.txt, waits for the overwrite prompt (proving the worker is in progress), cancels via the progress window (RequestCancellation), then dismisses the still-open conflict with IDCANCEL and confirms cancellation if prompted. - Tests: In-progress cancellation at a conflict, as opposed to declining the conflict (which journals as failure).
- Confirms: Target still has
cancel-conflict-target-content; source still has source content; journal containsOPERATION|cancelled. - Assumptions: Progress-window cancel is what writes
OPERATION|cancelled; the worker cannot unwind until the parked conflict prompt is answered. - Out of scope: Mid-byte cancellation without a prompt, and kill-process recovery.
Parameterized: Create Directory (cancelled-directory), Copy (cancel-copy.txt), Move (cancel-move.txt), Rename (cancelled-rename.txt).
- Does: Opens the corresponding operation dialog and closes it without committing (
commit: false). - Tests: Dialog-level cancel before the worker starts.
- Confirms: Create does not create the directory; copy/move do not create a target; rename does not create the new name; existing sources remain.
- Assumptions: Cancel is the dialog Close/Cancel path, not progress-window cancel.
- Out of scope: Cancel after the worker has started (see the conflicting-copy case).
- Does: Submits Create Directory with name
create-collision, which is already a file, then cancels the resulting failure UI. - Tests: Failed create-as-directory against an existing file inside the workspace.
- Confirms: File content remains
create-collision-content. - Assumptions: Failure stays on the sandbox path; the harness uses
SubmitInvalidPathAndCancel. - Out of scope: Privilege errors and disk-full.
Parameterized: Copy copy-file.txt and Move move-file.txt to blocked-target\child.txt, where blocked-target is a file.
- Does: Submits an impossible child path under a file and cancels the failure.
- Tests: Invalid destination for copy and move without leaving the sandbox.
- Confirms: Source still exists;
blocked-targetstill exists as the seeded file. - Assumptions: Destination is inside the workspace; it cannot succeed, so no child is created outside the sandbox.
- Out of scope: Network paths, ACL denial, and partial copy rollback beyond “source untouched.”
- Does: Renames
rename-file.txtontorename-collision.txt, chooses IDNO on the overwrite prompt, then cancels the reopened rename dialog. - Tests: Rename-specific skip (IDNO) returning to the rename dialog without mutating files.
- Confirms: Original and collision contents are unchanged.
- Assumptions: IDNO is skip for rename, not the copy/move Skip control; the rename dialog reappears with a Cancel button (automation ID
2). - Out of scope: Confirmed overwrite (separate case).
- Does: Attempts to rename
rename-collision-sourcetorename-collision-targetand cancels the failure. - Tests: Directory-directory name collision is rejected rather than offered file overwrite.
- Confirms: Both trees’
payload.txtcontents remain as seeded. - Assumptions: Directory overwrite is not a supported prompt in this product path.
- Out of scope: Merging directory trees.
- Does: Commits Configuration to immediate (Shift+Delete-style) deletion, holds
delete-locked.txtopen, selects it together withdelete-z-after-skip.txt, deletes, confirms, and Skip on IDRETRY/Skip (IDB_SKIP). - Tests: Product delete engine Retry/Skip after a sharing violation, continuing with later selected items.
- Confirms: The later file is gone; the locked file remains.
- Assumptions: Recycle Bin must be off for this engine: with the bin enabled the panel hands the selection to the shell (
CFilesWindow::DeleteThroughRecycleBin), which would show “File In Use” instead of Skip. Alphabeticaldelete-z-after-skip.txtis processed after the locked file. - Out of scope: Recycle Bin locked-file UI, Skip All, and Retry until success.
- Does: Seeds 2,048 files in
delete-large-flat-directorybefore launch, selects the directory, deletes, and confirms. - Tests: Per-entry delete of a large flat directory without putting that cost on every ordinary file-operation case.
- Confirms: The directory eventually no longer exists.
- Assumptions: Confirmation answered; timeout is long enough for a realistic high-entry delete; workspace seed runs before panel enumeration.
- Out of scope: Progress correctness, cancellation mid-directory, Recycle Bin of 2,048 items, and memory/handle budgets during the delete.
- Does: Relies on fixture launch (possibly under Application Verifier) and checks the main window class.
- Tests: Process startup under the selected verifier layer, isolated from the seven lifecycle scenarios.
- Confirms: Class name is
SalamanderMainWindowVer25. - Assumptions: Category
VerifierStartup; verifier/PageHeap, when used, are applied by the nightly runner around the process, not by this assertion. - Out of scope: Heaps/locks/exceptions diagnostics themselves, and any command after startup.
- Does: Opens Customize Top Toolbar, asserts the three combo labels, selects Small/Medium/Large via native
CBN_SELCHANGE(not UIASelect()), reopening between sizes, waits 500 ms after Close for the 250 ms debounce, restarts, asserts Large is restored, then restores the incoming index. - Tests: Live toolbar icon-size changes and persistence of the last choice.
- Confirms: Combo items are exactly Small 16, Medium 24, Large 32; each index sticks after Close/reopen; after restart the selection is Large (index 2).
- Assumptions: Resource ID
2748; Close is automation ID1; English combo strings; native selection notification is required because UIA Select bypasses the legacy handler. - Out of scope: Pixel-perfect glyphs, Fluent SVG assets (see
ToolbarIconSizeContractTestsandverify-fluent-icon-coverage.ps1), and menu/app-icon independence.
- Does: Restarts FileManager
FILEMANAGER_UI_LEAK_CYCLEStimes (default 20, nightly 100, clamped 5–200), samples handle/GDI/USER/private-byte counts, ignores cycle 1 (plug-in install), and checks spread plus early-vs-late mean shift. - Tests: Cross-restart resource growth on clean start/stop.
- Confirms: After warm starts, spreads stay within 32 handles, 64 GDI, 12 USER, 64 MiB private bytes; mean shifts stay within 16 / 16 / 6 / 16 MiB.
- Assumptions: Category
Leak; currently alsoQuarantined(quarantined-ui-tests.json) because a remote run measured a 39-handle warm-start spread against the 32-handle budget while Verifier and lock stress passed. Budgets compare equivalent warm starts, not the first cold plug-in install. - Out of scope: Leaks that only appear while a command is in flight, GDI from user interaction, and calibrating whether a 39-handle spread is startup noise or a real leak (the quarantine expiry).
- Does: Requires
FILEMANAGER_UI_CONFIG_FAULT_INJECTION=1. Establishes a baseline checkbox, measures registry write count of a real commit, samples uniformly spaced payload writes plus five named phases (checksum,complete,generation-flush,selector,store-flush). For each point, arms a marker file only after startup, commits a toggle, expects exit code 121, restarts, and asserts the first checkbox is either complete baseline or complete candidate—not a mixture—then restores baseline. - Tests: Crash recovery of transactional configuration generations.
- Confirms: Fault injection actually stops at the requested boundary; restart never shows a mixed profile.
- Assumptions: Category
FaultInjection;FILEMANAGER_UI_ISOLATED=1so native hooks honorFILEMANAGER_CONFIG_FAULT_*; arm file prevents plug-in startup saves from consuming the boundary; named phases stay valid when plug-ins add thousands of snapshot values. - Out of scope: Every individual registry value, concurrent writers, and migrating foreign profile versions during the crash.
Requires FILEMANAGER_UI_CROSS_VOLUME_ROOT (runner uses writable D:\filemanager-testdata when available). The fixture deletes only its GUID child.
- Does: Moves
move-treebetween different volume roots, accepts the metadata-preservation gate (IDYES), waits for destination release then source removal. - Tests: Cross-volume move as copy-then-delete with an explicit metadata gate.
- Confirms: Nested payload is on the target with original content; source tree is gone.
- Assumptions: Category
CrossVolume; volumes differ; timestamps often cannot be preserved, so the gate is expected. - Out of scope: ADS (next case), FAT targets, and network drives.
- Does: Writes two named streams on
ads-cross-volume.txt, moves across volumes, accepts the metadata gate, waits for destination then source deletion. - Tests: ADS preservation when both volumes support ADS.
- Confirms: Source is gone;
firstandsecondstreams match on the target. - Assumptions: Categories
CrossVolumeandAlternateDataStreams; both roots are ADS-capable (typically NTFS on C: and D:). - Out of scope: ADS-unsupported targets (quarantined fixture below).
- Does: Moves
ads-unsupported-target.txtto a FAT/FAT32/exFAT-like volume, confirms ADS-loss copy of the default stream (captionConfirm Alternate Data Streams Loss, IDYES), then declines the following metadata-loss Question (IDNO). - Tests: Default data published to an ADS-incapable target while declining source deletion so named streams are not lost.
- Confirms: Target has default content and no
must-not-silently-disappearstream; source still exists with that stream. - Assumptions: Categories
CrossVolume,AlternateDataStreams, and currentlyQuarantinedbecause a remote run selected inheritedads-overwrite.txtand showed overwrite instead of ADS-loss. RequiresFILEMANAGER_UI_ADS_UNSUPPORTED_TARGET_ROOT; source NTFS, target not ADS-capable; target must not already contain the name (overwrite would prove a panel-selection race). - Out of scope: Accepting source deletion after ADS loss, and NTFS-to-NTFS moves.
- Does: Before launch, writes
SALCPrestart-reconciled.tmpand an incomplete.opj(STATE|0|temporary-ready) pointing atrestart-reconciled.txt. Allows a disabled main window at startup. Answers Resume (IDYES) then the recovery summary (IDOK). - Tests: Real startup reconciliation of a ready transactional sibling.
- Confirms: Target exists with
recovered-after-restart; temporary file is gone; journal containsOPERATION|reconciled. - Assumptions: Category
Recovery; this fixture must not call the base journal purge before start; leftover journals from other tests are exactly what this scenario simulates on purpose. - Out of scope: Incomplete temps that are not
temporary-ready, user choosing not to resume, and multi-item journals.
Topology is created before launch. Junction/symlink targets live under the workspace root but outside the selected operation root. Category ReparsePoints.
- Does: Copies
reparse-operation-root, which contains a junction retargeted fromoutside-first-targettooutside-changed-targetand a junction that points back at the operation root. - Tests: Copy planner must not follow changed or cyclic directory junctions.
- Confirms:
inside.txtis copied; outside sentinels are unchanged;changed-junctionandcycle-junctionare not materialized on the target. - Assumptions:
mklink /Jworks without extra privilege; retargeting by delete-and-recreate is visible to the product as a changed reparse target. - Out of scope: Copying the junction itself as a reparse point, and mount points.
- Does: Deletes
delete-junctionwhose target isdelete-targetwithsentinel.txt. - Tests: Junction delete removes the link only.
- Confirms: The junction path is gone; the target sentinel content is unchanged.
- Assumptions: Confirmation answered; cleanup similarly must not recurse into junction targets.
- Out of scope: Recycle Bin of junctions and symlink delete.
- Does: Copies the same operation root when a directory symlink
outside-symlinkcould be created. - Tests: Copy must not materialize or traverse a directory symbolic link to an outside target.
- Confirms:
inside.txtcopied; outside sentinel unchanged;outside-symlinkabsent on the target. - Assumptions:
SeCreateSymbolicLinkPrivilege(or equivalent); otherwiseAssert.Ignorewith an explicit capability message. Release-equivalentruntests.ps1skips the whole UI lane if the privilege is missing rather than reporting partial completion. - Out of scope: File symlinks, junction coverage (always attempted), and creating links as the copy result.
These cases record current product behavior for reported defects. A failed assertion is valid evidence the defect remains; a stalled or crashed host is not acceptable. They must not install plug-ins or change application behavior to make an assertion pass.
- Does: Seeds
zip-open-characterization.zipwithzip-open-payload.txt, selects the archive, issues Open, dismisses an optional information dialog (IDOK within 3 s), waits until the main-window title contains the zip name, then quick-searches the payload name. - Tests: ZIP plug-in navigation after the information dialog.
- Confirms: The panel has navigated into the archive (title + listed payload), not merely a decorative title change.
- Assumptions:
FILEMANAGER_UI_ZIP_PLUGIN=1confirms the Zip plug-in is deployed and enabled. - Out of scope: Extract, nested archives, passworded zip, and fixing the reported defect if navigation still fails.
- Does: Issues Help Search, finds HTML Help (
HH Parent), typesFILEMANAGER_UI_HELP_SEARCH_TERM, presses Enter, waits for a descendant whose name containsFILEMANAGER_UI_HELP_EXPECTED_RESULT, then closes only that captured HH window. - Tests: Language-specific compiled help search against the deployed
salamand.chm. - Confirms: The configured existing result appears in the Help UI.
- Assumptions: Both Help environment variables are set;
help/<language>/salamand.chmexists beside the executable. - Out of scope: Full-text ranking, other CHMs, and installing help during the test.
Loopback HTTP/FTP/FTPS/stall cases in the parent fixture never launch FileManager. The nested fixture does.
- Does: Starts a loopback FTP server that sends a split multiline
220greeting then waits for one line and disconnects. Opens the product Connect to FTP Server dialog and connects to127.0.0.1:port. - Tests: Native FTP reply reader across socket-read boundaries before login.
- Confirms: The first client command starts with
USER; the scripted server completes. - Assumptions: Isolated profile; Connect command ID is available; 10 s bound for login command and server completion.
- Out of scope: Password/login success, TLS, data channel, bookmarks, and the live MojeRzeczy server.
Explicit, LiveFtp, excluded from scripts/runtests.ps1 and CI. Run via scripts/run-ftp-test.ps1.
Quick_connect_downloads_skan_txt_with_explicit_ftps_passive_binary_transfer_and_an_invalid_certificate
- Does: If
MOJERZEC_USERNAME/MOJERZEC_PASSWORDare missing, passes immediately with a message that FTP UI tests were not performed. Otherwise quick-connects toftp.mojerzeczy.comwith explicit FTPS (port 21, passive, binary, AUTH TLS), accepts a hostname-invalid certificate for this session only (does not persist the exception), dismisses Welcome Message, copies/skan.txtinto a GUID folder under the test-data root (queue checkbox off), waits until the file can be opened withFileShare.Noneat the reference size fromC:\Projects\FtpMojerzeczy\skan.txt. Debug error dialogs are logged underTestResults\ftp-debug-error-dialogs(orFILEMANAGER_UI_FTP_DEBUG_ERROR_LOG_DIRECTORY), dismissed, and fail the test. - Tests: Live explicit FTPS download against that one server, including invalid-certificate handling in the disposable profile.
- Confirms: Downloaded
skan.txtbyte length matches the local reference file. - Assumptions: External network; reference file exists and is non-empty; left panel is the post-connect remote listing; certificate policy must not be reused for other servers.
- Out of scope: Upload, resume, other hosts, content equality beyond size, and CI/release-gate execution.
Pull-request CI runs the four changed-line ratchets, operation-completion and durable-copy contracts, zlib and bzip2 probes, cmark-gfm hardening, and the SQLite recovery probe after its Debug x64 build. The root runner additionally executes the built 7-Zip wrapper/oracle compatibility corpus whenever a 7z.exe-compatible oracle is available; the release gate requires it.
The release workflow gates installer publication on the complete root runner using the dedicated filemanager-ui self-hosted environment. It supplies the built executable and SQLite DLL, enables configuration fault injection and Recycle Bin coverage, detects fixed writable D:\ for second-volume coverage, and uses -FailOnSkipped; known second-volume and missing-privilege environment skips are explicitly reported and allowed. When SeCreateSymbolicLinkPrivilege is unavailable, the complete UI suite is skipped as not completed rather than run partially. The release therefore fails if any runner check fails or any other NUnit case reports Assert.Ignore/NotExecuted. It retrieves Inno Setup only through tools/release-inputs.json, checks the locked SHA-256 and Authenticode publisher before installation, then passes an immutable uploaded installer artifact to the production-protected publish job. Private PDB artifacts are retained for 180 days and are never attached to the public release.
The dedicated runner must provide Visual Studio 2026, .NET 10, PowerShell 7.4+, Application Verifier, and an unlocked desktop. Symlink creation privileges are required for completed UI coverage; if they are absent, the runner reports the UI lane as not completed and skips it without failing the release gate. A fixed writable D:\ is optional: without it, the runner reports all second-volume-dependent tests as successful capability skips.
The nightly native-verifier lane runs the complete UI category with Application Verifier's Heaps, Handles, Locks, and Exceptions layers plus full PageHeap. gflags.exe is required and both Verifier and PageHeap settings are cleared in finally even if a test fails. The existing runner profile remains dedicated; provisioning and destroying a fresh Windows profile or VM is still an external runner-management requirement.