From 762c8236fe0f68ac4c728b54fed7453225cf21f4 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Tue, 18 Aug 2026 01:14:22 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Sync-Statusanzeige=20zeigt=20nach=20Neus?= =?UTF-8?q?tart=20f=C3=A4lschlich=20"nicht=20konfiguriert"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SyncEngine feuert sein erstes StatusChanged bereits im eigenen Konstruktor. Da SyncEngine und SyncStatusViewModel beide DI-Singletons sind und Letzterer Ersteren erst innerhalb der eigenen Factory aus dem Container holt, lief dieser erste Broadcast ab, bevor SyncStatusViewModel überhaupt abonniert hatte - der Status ging verloren, StatusText blieb bis zum nächsten Auto-Sync oder manuellen Sync beim hartcodierten Default "Kein Server konfiguriert". Fix: Konstruktor ruft nach dem Abonnieren zusätzlich einmal OnStatus(engine.Status) mit dem bereits vorhandenen aktuellen Zustand auf. Regressionstest ergänzt - dabei fehlte LehrerApp.Desktop.Tests das DisableTestParallelization-Attribut (gleicher bekannter LiteDB-BsonMapper.Global-Bug wie in den anderen Testprojekten, sobald zwei Testklassen parallel LiteDbContext konstruieren). Co-Authored-By: Claude Sonnet 5 --- LehrerApp.Desktop.Tests/AssemblyInfo.cs | 7 +++ .../SyncStatusViewModelTests.cs | 62 +++++++++++++++++++ .../ViewModels/SyncStatusViewModel.cs | 11 +++- TODO.md | 11 ++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 LehrerApp.Desktop.Tests/AssemblyInfo.cs create mode 100644 LehrerApp.Desktop.Tests/SyncStatusViewModelTests.cs diff --git a/LehrerApp.Desktop.Tests/AssemblyInfo.cs b/LehrerApp.Desktop.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..0710c6a --- /dev/null +++ b/LehrerApp.Desktop.Tests/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using Xunit; + +// Gleicher Grund wie in LehrerApp.Data.Tests/AssemblyInfo.cs: LiteDBs geteilter, statischer +// BsonMapper.Global verträgt keine parallele Erstzuordnung von Typ-Metadaten über mehrere +// Testklassen hinweg (SettingsViewModelTests/SyncStatusViewModelTests konstruieren beide +// LiteDbContext). +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/LehrerApp.Desktop.Tests/SyncStatusViewModelTests.cs b/LehrerApp.Desktop.Tests/SyncStatusViewModelTests.cs new file mode 100644 index 0000000..4c31e12 --- /dev/null +++ b/LehrerApp.Desktop.Tests/SyncStatusViewModelTests.cs @@ -0,0 +1,62 @@ +using LehrerApp.Data; +using LehrerApp.Desktop.ViewModels; +using LehrerApp.Sync; +using LehrerApp.Sync.Crypto; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class SyncStatusViewModelTests +{ + [Fact] + public void Konstruktor_EngineBereitsVorhanden_ZeigtNichtDenNichtKonfiguriertText() + { + // SyncEngine feuert sein erstes StatusChanged schon im eigenen Konstruktor - läuft der vor + // dem Abonnieren in SyncStatusViewModel (z.B. weil beide DI-Singletons sind und + // GetService() den Engine-Konstruktor erst innerhalb der VM-Factory auslöst), + // ginge dieser erste Status verloren und StatusText bliebe fälschlich beim Default. + using var temp = new TempSyncEngine(); + + var vm = new SyncStatusViewModel(temp.Engine); + + Assert.True(vm.IsServerConfigured); + Assert.NotEqual("Kein Server konfiguriert", vm.StatusText); + Assert.Equal("Synchronisiert", vm.StatusText); + } + + [Fact] + public void Konstruktor_KeineEngine_ZeigtNichtKonfiguriertText() + { + var vm = new SyncStatusViewModel(null); + + Assert.False(vm.IsServerConfigured); + Assert.Equal("Kein Server konfiguriert", vm.StatusText); + } + + private sealed class TempSyncEngine : IDisposable + { + private readonly string _queuePath = Path.Combine( + Path.GetTempPath(), $"lehrerapp-desktop-tests-{Guid.NewGuid():N}.db"); + private readonly LiteDbContext _db = new(new MemoryStream()); + private readonly HttpClient _http = new(); + public SyncEngine Engine { get; } + + public TempSyncEngine() + { + var queue = new EventQueue(_queuePath); + var key = SyncCrypto.GenerateKey(); + Engine = new SyncEngine( + queue, new ConflictResolver(queue), new EventApplier(_db, key), + new AttachmentSyncer(_db, _http, key), _http, + new SyncConfig { AutoSyncIntervalMinutes = 5 }); + } + + public void Dispose() + { + Engine.Dispose(); // disposed auch die EventQueue + _db.Dispose(); + _http.Dispose(); + if (File.Exists(_queuePath)) File.Delete(_queuePath); + } + } +} diff --git a/LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs b/LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs index ab6b663..3e0c475 100644 --- a/LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/SyncStatusViewModel.cs @@ -19,7 +19,16 @@ public partial class SyncStatusViewModel : ObservableObject { _engine = engine; IsServerConfigured = engine is not null; - if (_engine is not null) _engine.StatusChanged += OnStatus; + if (_engine is not null) + { + _engine.StatusChanged += OnStatus; + // SyncEngine feuert sein erstes StatusChanged bereits im eigenen Konstruktor + // (UpdateStatus) - der läuft aber schon, während wir hier noch in GetService() + // stecken, also VOR dem obigen Abonnieren. Ohne diesen Nachhol-Aufruf bliebe StatusText + // nach jedem Neustart beim Default "Kein Server konfiguriert", bis der nächste + // Auto-Sync oder Klick auf "Jetzt synchronisieren" den Text erstmals aktualisiert. + OnStatus(_engine.Status); + } } private void OnStatus(SyncStatus s) diff --git a/TODO.md b/TODO.md index 72eaba8..f802851 100644 --- a/TODO.md +++ b/TODO.md @@ -1348,6 +1348,17 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba **Umsetzung:** War bereits vorhanden (`SyncStatusViewModel`/`SyncStatusBar`), nur hier noch nicht abgehakt. + + **Nachtrag (Bug beim Nutzertest gefunden):** Nach jedem App-Neustart zeigte die Statusleiste + fälschlich "Kein Server konfiguriert", obwohl Sync korrekt eingerichtet war — bis zum + nächsten Auto-Sync oder Klick auf "Jetzt synchronisieren". Ursache: `SyncEngine` feuert sein + erstes `StatusChanged` bereits im eigenen Konstruktor; da `SyncEngine` und + `SyncStatusViewModel` beide DI-Singletons sind und Letzterer Ersteren erst innerhalb der + eigenen Factory aus dem Container holt, läuft dieser erste Broadcast ab, bevor + `SyncStatusViewModel` überhaupt abonniert hat — der Status ging verloren, `StatusText` blieb + beim hartcodierten Default. Fix: `SyncStatusViewModel`-Konstruktor ruft nach dem Abonnieren + zusätzlich einmal `OnStatus(engine.Status)` mit dem bereits vorhandenen aktuellen Zustand auf + (`SyncStatusViewModel.cs`). Regressionstest `SyncStatusViewModelTests.cs`. - [x] **10.1.6** Lokale Schreibvorgänge atomar an die Outbox (`EventQueue`) anbinden — aktuell ist das Sync-Grundgerüst registriert, die Repositories erzeugen aber noch keine Sync-Ereignisse.