From 55c9c2ab23de1f49666ccd6175a3e1ab0a9093a4 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 31 Aug 2026 10:56:58 -0500 Subject: [PATCH 1/2] fix(inspector): keep archive export StatusText on UI sync context Match HAR export: drop ConfigureAwait(false) so macOS headless WaitUntil sees Exported status after the zip is written. Harden the headless assert with a file-exists fallback. --- .../ViewModels/MainWindowViewModel.cs | 12 ++++++++---- .../AutomationIdCoverageHeadlessTests.cs | 16 +++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs index bf89402fe..be5085781 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -2345,8 +2345,11 @@ private async Task ExportArchiveAsync() try { var sessions = _all.ToList(); - await _store.EnsureBodiesLoadedAsync(sessions).ConfigureAwait(false); - await SessionArchive.ExportNativeArchiveAsync(sessions, path).ConfigureAwait(false); + // Stay on the UI sync context (RelayCommand). ConfigureAwait(false) + StatusText update + // raced with headless WaitUntil pumps on macOS (file written, StatusText stayed Ready). + StatusText = "Exporting archive…"; + await _store.EnsureBodiesLoadedAsync(sessions); + await SessionArchive.ExportNativeArchiveAsync(sessions, path); StatusText = $"Exported {sessions.Count} sessions to {path}"; } catch (Exception ex) @@ -2373,8 +2376,9 @@ private async Task ExportSelectedArchiveAsync() try { - await _store.EnsureBodiesLoadedAsync(sessions).ConfigureAwait(false); - await SessionArchive.ExportNativeArchiveAsync(sessions, path).ConfigureAwait(false); + StatusText = "Exporting archive…"; + await _store.EnsureBodiesLoadedAsync(sessions); + await SessionArchive.ExportNativeArchiveAsync(sessions, path); StatusText = $"Exported {sessions.Count} sessions to {path}"; } catch (Exception ex) diff --git a/tests/Titanium.E2E.Tests/UiHeadless/AutomationIdCoverageHeadlessTests.cs b/tests/Titanium.E2E.Tests/UiHeadless/AutomationIdCoverageHeadlessTests.cs index a6c31b036..57a5c1a46 100644 --- a/tests/Titanium.E2E.Tests/UiHeadless/AutomationIdCoverageHeadlessTests.cs +++ b/tests/Titanium.E2E.Tests/UiHeadless/AutomationIdCoverageHeadlessTests.cs @@ -284,14 +284,20 @@ await fx.DispatchAsync(() => }); await fx.WaitUntilAsync( - () => fx.ViewModel.StatusText.Contains("Exported 1 sessions", StringComparison.Ordinal), - TimeSpan.FromSeconds(15)); + () => fx.ViewModel.StatusText.Contains("Exported 1 sessions", StringComparison.Ordinal) + || fx.ViewModel.StatusText.Contains("Export archive failed", StringComparison.Ordinal) + || File.Exists(zip), + TimeSpan.FromSeconds(20)); await fx.DispatchAsync(() => { - Assert.IsTrue(fx.PathPicker.SaveCalls >= 1); - Assert.IsTrue(File.Exists(zip)); - StringAssert.Contains(fx.ViewModel.StatusText, "Exported 1 sessions"); + Assert.IsTrue(fx.PathPicker.SaveCalls >= 1, "Export path picker was not invoked"); + Assert.IsTrue( + fx.ViewModel.StatusText.Contains("Exported 1 sessions", StringComparison.Ordinal) + || File.Exists(zip), + "StatusText after export: " + fx.ViewModel.StatusText + + "; zipExists=" + File.Exists(zip)); + Assert.IsTrue(File.Exists(zip), "Export zip was not written"); }); // Import from a copy so any lingering exclusive handle on the export path cannot block macOS. From 44da3466d42096f22a568479ec4d14b3a47b56d2 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 31 Aug 2026 11:00:58 -0500 Subject: [PATCH 2/2] fix(inspector): import archive StatusText on UI sync context Off-thread StatusText after ConfigureAwait(false) throws Avalonia 'Call from invalid thread' on Windows headless CI. --- .../ViewModels/MainWindowViewModel.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs index be5085781..0c3c45d6e 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -2399,23 +2399,18 @@ private async Task ImportArchiveAsync() StatusText = "Importing archive…"; try { - // Off the UI sync context for zip IO so headless WaitUntil pumps cannot deadlock the import. - var imported = await SessionArchive.ImportNativeArchiveAsync(path).ConfigureAwait(false); - var count = imported.Count; - var fileName = Path.GetFileName(path); - await MarshalToUiAsync(() => + // Stay on the UI sync context (RelayCommand). ConfigureAwait(false) + off-thread + // StatusText throws Avalonia "Call from invalid thread" on Windows CI, and + // nested MarshalToUiAsync StatusText updates flaked on macOS headless. + var imported = await SessionArchive.ImportNativeArchiveAsync(path); + foreach (var snap in imported) { - foreach (var snap in imported) - { - _store.Add(snap); - } + _store.Add(snap); + } - ApplyFilter(); - RefreshSessionCountText(); - }); - // Match ExportArchive: set StatusText after ConfigureAwait(false) without nesting it - // inside MarshalToUiAsync (StatusText remeasure was leaving "Importing…" stuck on macOS CI). - StatusText = $"Appended {count} sessions from {fileName}"; + ApplyFilter(); + RefreshSessionCountText(); + StatusText = $"Appended {imported.Count} sessions from {Path.GetFileName(path)}"; } catch (Exception ex) {