From fe8f8a896354d467a5f0e943f1be6383330751b6 Mon Sep 17 00:00:00 2001 From: Andrei Mihai Visalon <54636077+Prekzursil@users.noreply.github.com> Date: Sun, 29 Mar 2026 08:22:37 +0000 Subject: [PATCH 01/30] Close CSM app coverage branches --- .github/workflows/codecov-analytics.yml | 4 +- .github/workflows/quality-zero-gate.yml | 4 +- .github/workflows/quality-zero-platform.yml | 4 +- .../MainWindowActionCoverageTests.cs | 616 ++++++++++++++++++ 4 files changed, 622 insertions(+), 6 deletions(-) create mode 100644 tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs diff --git a/.github/workflows/codecov-analytics.yml b/.github/workflows/codecov-analytics.yml index 183a7e5..30c0fbc 100644 --- a/.github/workflows/codecov-analytics.yml +++ b/.github/workflows/codecov-analytics.yml @@ -16,13 +16,13 @@ jobs: permissions: contents: read id-token: write - uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-codecov-analytics.yml@ab80e05c2acee987ad17d98e960c59966a4393eb + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-codecov-analytics.yml@8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 with: repo_slug: ${{ github.repository }} event_name: ${{ github.event_name }} sha: ${{ github.event.pull_request.head.sha || github.sha }} runner: windows-latest platform_repository: Prekzursil/quality-zero-platform - platform_ref: ab80e05c2acee987ad17d98e960c59966a4393eb + platform_ref: 8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/quality-zero-gate.yml b/.github/workflows/quality-zero-gate.yml index 694fcbb..7802e13 100644 --- a/.github/workflows/quality-zero-gate.yml +++ b/.github/workflows/quality-zero-gate.yml @@ -14,10 +14,10 @@ jobs: aggregate-gate: permissions: contents: read - uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-quality-zero-gate.yml@ab80e05c2acee987ad17d98e960c59966a4393eb + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-quality-zero-gate.yml@8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 with: repo_slug: ${{ github.repository }} event_name: ${{ github.event_name }} sha: ${{ github.event.pull_request.head.sha || github.sha }} platform_repository: Prekzursil/quality-zero-platform - platform_ref: ab80e05c2acee987ad17d98e960c59966a4393eb + platform_ref: 8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 diff --git a/.github/workflows/quality-zero-platform.yml b/.github/workflows/quality-zero-platform.yml index bbe7582..dcdf184 100644 --- a/.github/workflows/quality-zero-platform.yml +++ b/.github/workflows/quality-zero-platform.yml @@ -18,7 +18,7 @@ jobs: contents: read id-token: write pull-requests: write - uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-scanner-matrix.yml@ab80e05c2acee987ad17d98e960c59966a4393eb + uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-scanner-matrix.yml@8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 with: repo_slug: ${{ github.repository }} event_name: ${{ github.event_name }} @@ -29,7 +29,7 @@ jobs: sha: ${{ github.event.pull_request.head.sha || github.sha }} coverage_runner: windows-latest platform_repository: Prekzursil/quality-zero-platform - platform_ref: ab80e05c2acee987ad17d98e960c59966a4393eb + platform_ref: 8ad2ccf93ee4d4aed5ee9be9723e1047c3bd5261 secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} diff --git a/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs b/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs new file mode 100644 index 0000000..a32b4d8 --- /dev/null +++ b/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs @@ -0,0 +1,616 @@ +#pragma warning disable S3990 // Codacy false positive: the containing assembly declares CLSCompliant(true). +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Threading; +using CodexSessionManager.App; +using CodexSessionManager.Core.Maintenance; +using CodexSessionManager.Core.Sessions; +using CodexSessionManager.Core.Transcripts; +using CodexSessionManager.Storage.Discovery; +using CodexSessionManager.Storage.Indexing; +using CodexSessionManager.Storage.Maintenance; +using CodexSessionManager.Storage.Parsing; +using Microsoft.Win32; + +namespace CodexSessionManager.App.Tests; + +[SuppressMessage("Code Smell", "S2333", Justification = "The coverage tests are intentionally split across partial files.")] +public sealed partial class MainWindowCoverageTests +{ + [Fact] + public void ExternalActionButtons_use_wrappers() + { + RunInSta(() => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-actions", "Actions Thread"); + var session = BuildIndexedSession("session-actions", "Actions Thread", sessionFile); + var window = new MainWindow(); + var started = new List<(string fileName, IReadOnlyList arguments)>(); + var copied = new List(); + var exportPath = Path.Combine(root, "export.md"); + + AddSession(window, session); + SelectSingleSession(window, GetNamedField(window, "SessionsListBox").Items.Cast().Single()); + GetNamedField(window, "CwdTextBlock").Text = @"C:\repo"; + GetNamedField(window, "ReadableTranscriptTextBox").Text = "exported transcript"; + SetProvider(window, "ProcessStarter", ((Action>)((fileName, arguments) => started.Add((fileName, arguments))))); + SetProvider(window, "ClipboardSetter", ((Action)(text => copied.Add(text)))); + SetProvider(window, "ExportPathSelector", ((Func)(_ => exportPath))); + SetProvider(window, "TextFileWriter", ((Action)((fileName, contents) => File.WriteAllText(fileName, contents, Encoding.UTF8)))); + + OpenFolderMethod.Invoke(window, [window, new RoutedEventArgs()]); + OpenRawMethod.Invoke(window, [window, new RoutedEventArgs()]); + CopyPathMethod.Invoke(window, [window, new RoutedEventArgs()]); + ResumeMethod.Invoke(window, [window, new RoutedEventArgs()]); + ExportMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Equal(3, started.Count); + Assert.Equal("explorer.exe", started[0].fileName); + Assert.Equal("notepad.exe", started[1].fileName); + Assert.Equal("codex", started[2].fileName); + Assert.Single(copied); + Assert.Equal(sessionFile, copied[0]); + Assert.Equal("exported transcript", File.ReadAllText(exportPath, Encoding.UTF8)); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public void OpenFolderButton_skips_launch_when_preferred_path_has_no_directory() + { + RunInSta(() => + { + var session = BuildIndexedSession("session-nodir", "No Dir", "session-nodir.jsonl"); + var window = new MainWindow(); + var started = new List<(string fileName, IReadOnlyList arguments)>(); + + AddSession(window, session); + SelectSingleSession(window, session); + SetProvider(window, "ProcessStarter", (Action>)((fileName, arguments) => started.Add((fileName, arguments)))); + + OpenFolderMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Empty(started); + window.Close(); + }); + } + + [Fact] + public void ResumeButton_uses_user_profile_when_cwd_is_not_available() + { + RunInSta(() => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-resume-default", "Resume Default"); + var session = BuildIndexedSession("session-resume-default", "Resume Default", sessionFile); + var window = new MainWindow(); + var started = new List<(string fileName, IReadOnlyList arguments)>(); + + AddSession(window, session); + SelectSingleSession(window, session); + GetNamedField(window, "CwdTextBlock").Text = "-"; + SetProvider(window, "ProcessStarter", (Action>)((fileName, arguments) => started.Add((fileName, arguments)))); + + ResumeMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Single(started); + Assert.Contains(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), started[0].arguments, StringComparer.OrdinalIgnoreCase); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ExportButton_with_cancelled_path_does_not_writeAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-export-cancel", "Export Cancel"); + var session = BuildIndexedSession("session-export-cancel", "Export Cancel", sessionFile); + var parsed = BuildParsedFile("session-export-cancel", @"C:\export"); + var window = new MainWindow(); + var wrote = false; + + AddSession(window, session); + SetProvider(window, "SessionParser", ((Func>)((_, _) => Task.FromResult(parsed)))); + SetProvider(window, "FileTextReader", ((Func)(_ => "raw-session-content"))); + SelectSingleSession(window, session); + GetNamedField(window, "ReadableTranscriptTextBox").Text = "ignored transcript"; + SetProvider(window, "ExportPathSelector", (Func)(_ => null)); + SetProvider(window, "TextFileWriter", (Action)((_, _) => wrote = true)); + + ExportMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.False(wrote); + Assert.Equal("Starting…", GetNamedField(window, "StatusTextBlock").Text); + window.Close(); + await Task.CompletedTask; + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ButtonHandlers_return_without_selection_or_previewAsync() + { + await RunInStaAsync(async () => + { + var window = new MainWindow(); + GetNamedField(window, "StatusTextBlock").Text = "idle"; + + OpenFolderMethod.Invoke(window, [window, new RoutedEventArgs()]); + OpenRawMethod.Invoke(window, [window, new RoutedEventArgs()]); + CopyPathMethod.Invoke(window, [window, new RoutedEventArgs()]); + ResumeMethod.Invoke(window, [window, new RoutedEventArgs()]); + ExportMethod.Invoke(window, [window, new RoutedEventArgs()]); + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + + Assert.Equal("idle", GetNamedField(window, "StatusTextBlock").Text); + window.Close(); + }); + } + + [Fact] + public void ExportPathSelector_uses_dialog_factory_and_presenter() + { + RunInSta(() => + { + var window = new MainWindow(); + SaveFileDialog? createdDialog = null; + + SetProvider(window, "SaveFileDialogFactory", (Func)(() => createdDialog = new SaveFileDialog())); + SetProvider(window, "SaveFileDialogPresenter", (Func)((dialog, owner) => + { + Assert.Same(window, owner); + Assert.Equal("session-1.md", dialog.FileName); + Assert.Equal("Markdown (*.md)|*.md|Text (*.txt)|*.txt|JSON (*.json)|*.json", dialog.Filter); + dialog.FileName = @"C:\exports\session-1.md"; + return true; + })); + + var exportSelector = (Delegate)typeof(MainWindow) + .GetProperty("ExportPathSelector", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(window)!; + var exportPath = (string?)exportSelector.DynamicInvoke("session-1.md"); + + Assert.NotNull(createdDialog); + Assert.Equal(@"C:\exports\session-1.md", exportPath); + window.Close(); + }); + } + + [Fact] + public void ExportButton_returns_when_selector_returns_blank_path() + { + RunInSta(() => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-export", "Export Thread"); + var session = BuildIndexedSession("session-export", "Export Thread", sessionFile); + var window = new MainWindow(); + var writes = new List(); + + AddSession(window, session); + SelectSingleSession(window, session); + Assert.NotNull(GetSelectedSession(window)); + GetNamedField(window, "ReadableTranscriptTextBox").Text = "ignored"; + GetNamedField(window, "StatusTextBlock").Text = "idle"; + SetProvider(window, "ExportPathSelector", (Func)(_ => " ")); + SetProvider(window, "TextFileWriter", (Action)((path, _) => writes.Add(path))); + + ExportMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Empty(writes); + Assert.Equal("idle", GetNamedField(window, "StatusTextBlock").Text); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public void SelectExportPath_returns_null_when_dialog_is_cancelled() + { + RunInSta(() => + { + var window = new MainWindow(); + SaveFileDialog? createdDialog = null; + + SetProvider(window, "SaveFileDialogFactory", (Func)(() => createdDialog = new SaveFileDialog())); + SetProvider(window, "SaveFileDialogPresenter", (Func)((_, owner) => + { + Assert.Same(window, owner); + return false; + })); + + var exportSelector = (Delegate)typeof(MainWindow) + .GetProperty("ExportPathSelector", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(window)!; + var exportPath = (string?)exportSelector.DynamicInvoke("session-2.md"); + + Assert.NotNull(createdDialog); + Assert.Null(exportPath); + window.Close(); + }); + } + + [Fact] + public void DescribeSqlitePath_returns_summary_for_existing_file() + { + var root = CreateTempDirectory(); + try + { + var sqlitePath = Path.Combine(root, "state_5.sqlite"); + File.WriteAllText(sqlitePath, "sqlite"); + + var description = (string?)DescribeSqlitePathMethod.Invoke(null, [sqlitePath, null]); + + Assert.NotNull(description); + Assert.Contains(sqlitePath, description, StringComparison.Ordinal); + Assert.Contains("MB", description, StringComparison.Ordinal); + } + finally + { + DeleteDirectory(root); + } + } + + [Fact] + public void DescribeSqlitePath_single_argument_overload_returns_summary_for_existing_file() + { + var root = CreateTempDirectory(); + try + { + var sqlitePath = Path.Combine(root, "state_5.sqlite"); + File.WriteAllText(sqlitePath, "sqlite"); + + var description = (string?)DescribeSqlitePathSingleArgumentMethod.Invoke(null, [sqlitePath]); + + Assert.NotNull(description); + Assert.Contains(sqlitePath, description, StringComparison.Ordinal); + } + finally + { + DeleteDirectory(root); + } + } + + [Fact] + public void SearchCancellation_helpers_replace_and_release_current_source() + { + RunInSta(() => + { + var window = new MainWindow(); + + var firstToken = (CancellationToken)BeginSearchTokenMethod.Invoke(window, [])!; + var firstSource = Assert.IsType( + CurrentSearchCancellationTokenSourceProperty.GetValue(window)); + + var secondToken = (CancellationToken)BeginSearchTokenMethod.Invoke(window, [])!; + var secondSource = Assert.IsType( + CurrentSearchCancellationTokenSourceProperty.GetValue(window)); + + Assert.True(firstToken.IsCancellationRequested); + Assert.NotSame(firstSource, secondSource); + Assert.Throws(() => _ = firstSource.Token.WaitHandle); + + ReleaseSearchCancellationStateMethod.Invoke(window, []); + + Assert.True(secondToken.IsCancellationRequested); + Assert.Null(CurrentSearchCancellationTokenSourceProperty.GetValue(window)); + + window.Close(); + }); + } + + [Fact] + public async Task BuildPreview_and_execute_maintenance_paths_update_uiAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-maint", "Maintenance Thread"); + var session = BuildIndexedSession("session-maint", "Maintenance Thread", sessionFile); + var window = new MainWindow(); + var destinationRoots = new List(); + + MaintenanceExecutorField.SetValue(window, new MaintenanceExecutor(Path.Combine(root, "checkpoints"))); + AddSession(window, session); + SelectSingleSession(window, GetNamedField(window, "SessionsListBox").Items.Cast().Single()); + GetNamedField(window, "MaintenanceActionComboBox").SelectedItem = MaintenanceAction.Reconcile; + SetProvider( + window, + "MaintenanceRunner", + ((Func>)((_, destinationRoot, _, _) => + { + destinationRoots.Add(destinationRoot); + return Task.FromResult(new MaintenanceExecutionResult(true, [], Path.Combine(root, "checkpoint.json"))); + }))); + + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + GetNamedField(window, "DestinationRootTextBox").Text = string.Empty; + + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + Assert.NotEmpty(destinationRoots); + Assert.Contains("Executed maintenance. Checkpoint:", GetNamedField(window, "StatusTextBlock").Text, StringComparison.Ordinal); + + SetProvider( + window, + "MaintenanceRunner", + ((Func>)((_, _, _, _) => + Task.FromException(new InvalidOperationException("blocked"))))); + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + Assert.Contains("Maintenance failed: blocked", GetNamedField(window, "StatusTextBlock").Text, StringComparison.Ordinal); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ExecuteMaintenanceAsync_uses_default_runner_when_executor_is_configuredAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-default-runner", "Default Runner"); + var session = BuildIndexedSession("session-default-runner", "Default Runner", sessionFile); + var window = new MainWindow(); + + MaintenanceExecutorField.SetValue(window, new MaintenanceExecutor(Path.Combine(root, "checkpoints"))); + AddSession(window, session); + SelectSingleSession(window, session); + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + GetNamedField(window, "DestinationRootTextBox").Text = string.Empty; + + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + + Assert.Contains("Executed maintenance. Checkpoint:", GetNamedField(window, "StatusTextBlock").Text, StringComparison.Ordinal); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ExecuteMaintenanceAsync_returns_without_executor_when_preview_existsAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-missing-executor", "Missing Executor"); + var session = BuildIndexedSession("session-missing-executor", "Missing Executor", sessionFile); + var window = new MainWindow(); + + AddSession(window, session); + SelectSingleSession(window, session); + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + GetNamedField(window, "StatusTextBlock").Text = "idle"; + + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + + Assert.Equal("idle", GetNamedField(window, "StatusTextBlock").Text); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public void BuildPreviewButton_uses_archive_fallback_and_plural_confirmation() + { + RunInSta(() => + { + var root = CreateTempDirectory(); + try + { + var sessionOne = BuildIndexedSession("session-a", "Thread A", WriteSessionJsonl(root, "session-a", "Thread A")); + var sessionTwo = BuildIndexedSession("session-b", "Thread B", WriteSessionJsonl(root, "session-b", "Thread B")); + var window = new MainWindow(); + var listBox = GetNamedField(window, "SessionsListBox"); + + AddSession(window, sessionOne); + AddSession(window, sessionTwo); + GetNamedField(window, "MaintenanceActionComboBox").SelectedItem = null; + + listBox.SelectedItems.Clear(); + listBox.SelectedItems.Add(sessionOne); + listBox.SelectedItems.Add(sessionTwo); + + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Contains("Confirm with: ARCHIVE 2 FILES", GetNamedField(window, "MaintenanceSummaryTextBlock").Text, StringComparison.Ordinal); + Assert.Equal("ARCHIVE 2 FILES", GetNamedField(window, "TypedConfirmationTextBox").Text); + window.Close(); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public void StartExternalProcess_rejects_blank_file_name_and_null_arguments() + { + var blankException = Assert.Throws(() => + StartExternalProcessMethod.Invoke(null, [" ", Array.Empty()])); + Assert.IsType(blankException.InnerException); + + var argumentsException = Assert.Throws(() => + StartExternalProcessMethod.Invoke(null, ["codex", null!])); + Assert.IsType(argumentsException.InnerException); + } + + [Fact] + public void StartExternalProcess_starts_valid_process() + { + var fileName = Path.Combine(Environment.SystemDirectory, "cmd.exe"); + + StartExternalProcessMethod.Invoke( + null, + [fileName, new[] { "/c", "exit", "0" }]); + } + + [Fact] + public void StartExternalProcess_starts_valid_process_without_arguments() + { + var fileName = Path.Combine(Environment.SystemDirectory, "whoami.exe"); + + StartExternalProcessMethod.Invoke( + null, + [fileName, Array.Empty()]); + } + + [Fact] + public async Task MaintenanceRunner_throws_when_executor_is_not_initializedAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-missing-runner", "Missing Runner"); + var session = BuildIndexedSession("session-missing-runner", "Missing Runner", sessionFile); + var window = new MainWindow(); + + AddSession(window, session); + SelectSingleSession(window, session); + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + + var preview = (MaintenancePreview)typeof(MainWindow) + .GetField("_currentMaintenancePreview", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(window)!; + var runner = (Func>)typeof(MainWindow) + .GetProperty("MaintenanceRunner", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(window)!; + + var exception = await Assert.ThrowsAsync(() => + runner(preview, Path.Combine(root, "archive"), string.Empty, CancellationToken.None)); + Assert.Equal("Maintenance executor has not been initialized.", exception.Message); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ExecuteMaintenanceAsync_preserves_nonblank_destination_rootAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-custom-destination", "Custom Destination"); + var session = BuildIndexedSession("session-custom-destination", "Custom Destination", sessionFile); + var requestedDestinationRoot = Path.Combine(root, "custom-archive"); + var observedDestinationRoots = new List(); + var window = new MainWindow(); + + MaintenanceExecutorField.SetValue(window, new MaintenanceExecutor(Path.Combine(root, "checkpoints"))); + AddSession(window, session); + SelectSingleSession(window, session); + SetProvider( + window, + "MaintenanceRunner", + ((Func>)((_, destinationRoot, _, _) => + { + observedDestinationRoots.Add(destinationRoot); + return Task.FromResult(new MaintenanceExecutionResult(false, [], Path.Combine(root, "checkpoint.json"))); + }))); + + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + GetNamedField(window, "DestinationRootTextBox").Text = requestedDestinationRoot; + + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + + Assert.Single(observedDestinationRoots); + Assert.Equal(requestedDestinationRoot, observedDestinationRoots[0]); + Assert.Equal("Maintenance did not execute.", GetNamedField(window, "StatusTextBlock").Text); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public async Task ExecuteMaintenanceAsync_sets_status_when_runner_returns_not_executedAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var sessionFile = WriteSessionJsonl(root, "session-maint-noexec", "Maintenance Thread"); + var session = BuildIndexedSession("session-maint-noexec", "Maintenance Thread", sessionFile); + var window = new MainWindow(); + + MaintenanceExecutorField.SetValue(window, new MaintenanceExecutor(Path.Combine(root, "checkpoints"))); + AddSession(window, session); + SelectSingleSession(window, session); + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + SetProvider( + window, + "MaintenanceRunner", + (Func>)((_, _, _, _) => + Task.FromResult(new MaintenanceExecutionResult(false, [], Path.Combine(root, "checkpoint.json"))))); + + await InvokePrivateTaskAsync(window, ExecuteMaintenanceUiAsyncMethod); + + Assert.Equal("Maintenance did not execute.", GetNamedField(window, "StatusTextBlock").Text); + } + finally + { + DeleteDirectory(root); + } + }); + } +} From 471d42f3dfdc34e5f0ca19ea29baca2e10bb0546 Mon Sep 17 00:00:00 2001 From: Andrei Mihai Visalon <54636077+Prekzursil@users.noreply.github.com> Date: Sun, 29 Mar 2026 08:53:25 +0000 Subject: [PATCH 02/30] Close CSM main quality gaps --- .../MainWindowActionCoverageTests.cs | 113 +- .../MainWindowCoverageTests.cs | 1226 +---------------- 2 files changed, 183 insertions(+), 1156 deletions(-) diff --git a/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs b/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs index a32b4d8..6870ab6 100644 --- a/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs +++ b/tests/CodexSessionManager.App.Tests/MainWindowActionCoverageTests.cs @@ -25,6 +25,8 @@ namespace CodexSessionManager.App.Tests; [SuppressMessage("Code Smell", "S2333", Justification = "The coverage tests are intentionally split across partial files.")] public sealed partial class MainWindowCoverageTests { + private static readonly string[] SuccessfulExitArguments = ["/c", "exit", "0"]; + [Fact] public void ExternalActionButtons_use_wrappers() { @@ -490,9 +492,12 @@ public void StartExternalProcess_starts_valid_process() { var fileName = Path.Combine(Environment.SystemDirectory, "cmd.exe"); - StartExternalProcessMethod.Invoke( - null, - [fileName, new[] { "/c", "exit", "0" }]); + var exception = Record.Exception(() => + StartExternalProcessMethod.Invoke( + null, + [fileName, SuccessfulExitArguments])); + + Assert.Null(exception); } [Fact] @@ -500,9 +505,105 @@ public void StartExternalProcess_starts_valid_process_without_arguments() { var fileName = Path.Combine(Environment.SystemDirectory, "whoami.exe"); - StartExternalProcessMethod.Invoke( - null, - [fileName, Array.Empty()]); + var exception = Record.Exception(() => + StartExternalProcessMethod.Invoke( + null, + [fileName, Array.Empty()])); + + Assert.Null(exception); + } + + [Fact] + public async Task Refresh_buttons_invoke_background_refresh_with_expected_modesAsync() + { + await RunInStaAsync(async () => + { + var root = CreateTempDirectory(); + try + { + var window = new MainWindow(); + var repository = CreateRepository(root); + var observedModes = new List(); + + RepositoryField.SetValue(window, repository); + WorkspaceIndexerField.SetValue(window, new SessionWorkspaceIndexer(repository)); + SetProvider( + window, + "KnownStoresProvider", + (Func>)(deepScan => + { + observedModes.Add(deepScan); + return Array.Empty(); + })); + + RefreshButtonMethod.Invoke(window, [window, new RoutedEventArgs()]); + DeepScanButtonMethod.Invoke(window, [window, new RoutedEventArgs()]); + + for (var attempt = 0; attempt < 50 && observedModes.Count < 2; attempt++) + { + await Task.Delay(10); + } + + Assert.Equal(2, observedModes.Count); + Assert.Contains(false, observedModes); + Assert.Contains(true, observedModes); + Assert.Contains( + "Indexed 0 deduped sessions", + GetNamedField(window, "StatusTextBlock").Text, + StringComparison.Ordinal); + } + finally + { + DeleteDirectory(root); + } + }); + } + + [Fact] + public void BuildPreview_ignores_selected_sessions_without_physical_copies() + { + RunInSta(() => + { + var root = CreateTempDirectory(); + try + { + var firstSessionFile = WriteSessionJsonl(root, "session-preview-available", "Preview Available"); + var secondSessionFile = WriteSessionJsonl(root, "session-preview-empty", "Preview Empty"); + var window = new MainWindow(); + var availableSession = BuildIndexedSession( + "session-preview-available", + "Preview Available", + firstSessionFile); + var emptySession = WithNullIndexedSessionProperty( + BuildIndexedSession( + "session-preview-empty", + "Preview Empty", + secondSessionFile), + nameof(IndexedLogicalSession.PhysicalCopies)); + var listBox = GetNamedField(window, "SessionsListBox"); + + AddSession(window, availableSession); + AddSession(window, emptySession); + listBox.SelectedItems.Clear(); + listBox.SelectedItems.Add(availableSession); + listBox.SelectedItems.Add(emptySession); + + BuildPreviewMethod.Invoke(window, [window, new RoutedEventArgs()]); + + Assert.Contains( + "Confirm with: ARCHIVE 1 FILE", + GetNamedField(window, "MaintenanceSummaryTextBlock").Text, + StringComparison.Ordinal); + Assert.Equal( + "ARCHIVE 1 FILE", + GetNamedField(window, "TypedConfirmationTextBox").Text); + window.Close(); + } + finally + { + DeleteDirectory(root); + } + }); } [Fact] diff --git a/tests/CodexSessionManager.App.Tests/MainWindowCoverageTests.cs b/tests/CodexSessionManager.App.Tests/MainWindowCoverageTests.cs index d5ef5e7..a7fe7dc 100644 --- a/tests/CodexSessionManager.App.Tests/MainWindowCoverageTests.cs +++ b/tests/CodexSessionManager.App.Tests/MainWindowCoverageTests.cs @@ -1,4 +1,6 @@ +#pragma warning disable S3990 // Codacy false positive: the containing assembly declares CLSCompliant(true). using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Text; @@ -6,6 +8,7 @@ using System.Threading; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Threading; using CodexSessionManager.App; using CodexSessionManager.Core.Maintenance; @@ -19,11 +22,15 @@ namespace CodexSessionManager.App.Tests; -public sealed class MainWindowCoverageTests +[SuppressMessage("Code Smell", "S2333", Justification = "The coverage tests are intentionally split across partial files.")] +public sealed partial class MainWindowCoverageTests { private static readonly MethodInfo BuildKnownStoresMethod = typeof(MainWindow).GetMethod("BuildKnownStores", BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo StartExternalProcessMethod = + typeof(MainWindow).GetMethod("StartExternalProcess", BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetLiveSqliteStatusMethod = typeof(MainWindow).GetMethods(BindingFlags.NonPublic | BindingFlags.Static) .Single(method => method.Name == "GetLiveSqliteStatus" && method.GetParameters().Length == 0); @@ -44,7 +51,33 @@ public sealed class MainWindowCoverageTests }); private static readonly MethodInfo DescribeSqlitePathMethod = - typeof(MainWindow).GetMethod("DescribeSqlitePath", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public)!; + typeof(MainWindow).GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public) + .Single(method => + { + if (method.Name != "DescribeSqlitePath") + { + return false; + } + + var parameters = method.GetParameters(); + return parameters.Length == 2 + && parameters[0].ParameterType == typeof(string) + && parameters[1].ParameterType == typeof(Func); + }); + + private static readonly MethodInfo DescribeSqlitePathSingleArgumentMethod = + typeof(MainWindow).GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public) + .Single(method => + { + if (method.Name != "DescribeSqlitePath") + { + return false; + } + + var parameters = method.GetParameters(); + return parameters.Length == 1 + && parameters[0].ParameterType == typeof(string); + }); private static readonly MethodInfo InitializeAsyncMethod = typeof(MainWindow).GetMethod("InitializeAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -66,6 +99,9 @@ public sealed class MainWindowCoverageTests .Single(method => method.Name == "RunOnUiThreadValueAsync" && method.IsGenericMethodDefinition) .MakeGenericMethod(typeof(string)); + private static readonly MethodInfo RunEventTaskMethod = + typeof(MainWindow).GetMethod("RunEventTask", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo LoadSelectedSessionAsyncMethod = typeof(MainWindow).GetMethod("LoadSelectedSessionAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; @@ -78,9 +114,21 @@ public sealed class MainWindowCoverageTests private static readonly MethodInfo SearchSessionsAsyncMethod = typeof(MainWindow).GetMethod("SearchSessionsAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo ReloadSessionsForSearchAsyncMethod = + typeof(MainWindow).GetMethod("ReloadSessionsForSearchAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo ApplySearchResultsAsyncMethod = + typeof(MainWindow).GetMethod("ApplySearchResultsAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo SaveSelectedMetadataAsyncMethod = typeof(MainWindow).GetMethod("SaveSelectedMetadataAsync", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo BeginSearchTokenMethod = + typeof(MainWindow).GetMethod("BeginSearchToken", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo ReleaseSearchCancellationStateMethod = + typeof(MainWindow).GetMethod("ReleaseSearchCancellationState", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo ExecuteMaintenanceUiAsyncMethod = typeof(MainWindow).GetMethod( "ExecuteMaintenanceAsync", @@ -107,6 +155,30 @@ public sealed class MainWindowCoverageTests private static readonly MethodInfo BuildPreviewMethod = typeof(MainWindow).GetMethod("BuildPreviewButton_OnClick", BindingFlags.NonPublic | BindingFlags.Instance)!; + private static readonly MethodInfo GetSelectedSessionsMethod = + typeof(MainWindow).GetMethod("GetSelectedSessions", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo SessionsListSelectionChangedMethod = + typeof(MainWindow).GetMethod("SessionsListBox_OnSelectionChanged", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo SearchTextChangedMethod = + typeof(MainWindow).GetMethod("SearchTextBox_OnTextChanged", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo SaveMetadataButtonMethod = + typeof(MainWindow).GetMethod("SaveMetadataButton_OnClick", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo RefreshButtonMethod = + typeof(MainWindow).GetMethod("RefreshButton_OnClick", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo DeepScanButtonMethod = + typeof(MainWindow).GetMethod("DeepScanButton_OnClick", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo ExecuteMaintenanceButtonMethod = + typeof(MainWindow).GetMethod("ExecuteMaintenanceButton_OnClick", BindingFlags.NonPublic | BindingFlags.Instance)!; + + private static readonly MethodInfo GetRequiredPreferredCopyMethod = + typeof(MainWindow).GetMethod("GetRequiredPreferredCopy", BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly FieldInfo SessionsField = typeof(MainWindow).GetField("_sessions", BindingFlags.Instance | BindingFlags.NonPublic)!; @@ -119,1157 +191,11 @@ public sealed class MainWindowCoverageTests private static readonly FieldInfo MaintenanceExecutorField = typeof(MainWindow).GetField("_maintenanceExecutor", BindingFlags.Instance | BindingFlags.NonPublic)!; - private static readonly FieldInfo SearchCtsField = - typeof(MainWindow).GetField("_searchCts", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly PropertyInfo CurrentSearchCancellationTokenSourceProperty = + typeof(MainWindow).GetProperty("CurrentSearchCancellationTokenSource", BindingFlags.Instance | BindingFlags.NonPublic)!; private static readonly string[] SqliteStatusPaths = ["first", "second"]; - [Fact] - public void Constructor_initializes_core_bindings() - { - RunInSta(() => - { - var window = new MainWindow(); - - Assert.NotNull(GetNamedField(window, "SessionsListBox").ItemsSource); - Assert.Equal(MaintenanceAction.Archive, GetNamedField(window, "MaintenanceActionComboBox").SelectedItem); - Assert.Equal("Starting…", GetNamedField(window, "StatusTextBlock").Text); - }); - } - - [Fact] - public void InitializeComponent_rehydrates_named_controls() - { - RunInSta(() => - { - var window = new MainWindow(); - - window.InitializeComponent(); - - Assert.Equal("Codex Session Manager", window.Title); - }); - } - - [Fact] - public void InitializeComponent_can_be_called_twice_without_reloading_content() - { - RunInSta(() => - { - var window = new MainWindow(); - - window.InitializeComponent(); - - Assert.NotNull(GetNamedField