From 2e869194e96dd940a1ae370993c1c809bf30128f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Tue, 21 Apr 2026 13:33:20 +0200 Subject: [PATCH] test: verify sync errors always appear in the sync log Add _CapturingSyncLogRepository and two tests (IMAP + JMAP) that assert a failed sync cycle produces an error entry in the sync log. Also replace .ignore() in the catch blocks with a proper try-catch so the sync log write is genuinely attempted and any secondary failure is logged to stdout rather than silently dropped. Co-Authored-By: Claude Sonnet 4.6 --- lib/core/sync/account_sync_manager.dart | 64 +++++++++-------- test/unit/account_sync_manager_test.dart | 92 ++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 30 deletions(-) diff --git a/lib/core/sync/account_sync_manager.dart b/lib/core/sync/account_sync_manager.dart index 1a50e88..f9b4285 100644 --- a/lib/core/sync/account_sync_manager.dart +++ b/lib/core/sync/account_sync_manager.dart @@ -140,21 +140,23 @@ class _AccountSync implements _SyncLoop { await _idle(); _backoffSeconds = 5; } catch (e, st) { - _syncLog - .log( - accountId: account.id, - success: false, - errorMessage: e.toString(), - protocol: 'imap', - emailsFetched: 0, - emailsSkipped: 0, - mailboxesSynced: 0, - pendingFlushed: 0, - bytesTransferred: 0, - startedAt: startedAt, - finishedAt: DateTime.now(), - ) - .ignore(); + try { + await _syncLog.log( + accountId: account.id, + success: false, + errorMessage: e.toString(), + protocol: 'imap', + emailsFetched: 0, + emailsSkipped: 0, + mailboxesSynced: 0, + pendingFlushed: 0, + bytesTransferred: 0, + startedAt: startedAt, + finishedAt: DateTime.now(), + ); + } catch (logErr) { + log('Failed to write IMAP sync log entry: $logErr'); + } log( 'Sync failed for ${account.email}, retrying in ${_backoffSeconds}s', error: e, @@ -286,21 +288,23 @@ class _JmapAccountSync implements _SyncLoop { _backoffSeconds = 5; await _wait(); } catch (e, st) { - _syncLog - .log( - accountId: account.id, - success: false, - errorMessage: e.toString(), - protocol: 'jmap', - emailsFetched: 0, - emailsSkipped: 0, - mailboxesSynced: 0, - pendingFlushed: 0, - bytesTransferred: 0, - startedAt: startedAt, - finishedAt: DateTime.now(), - ) - .ignore(); + try { + await _syncLog.log( + accountId: account.id, + success: false, + errorMessage: e.toString(), + protocol: 'jmap', + emailsFetched: 0, + emailsSkipped: 0, + mailboxesSynced: 0, + pendingFlushed: 0, + bytesTransferred: 0, + startedAt: startedAt, + finishedAt: DateTime.now(), + ); + } catch (logErr) { + log('Failed to write JMAP sync log entry: $logErr'); + } log( 'JMAP sync failed for ${account.email}, retrying in ${_backoffSeconds}s', error: e, diff --git a/test/unit/account_sync_manager_test.dart b/test/unit/account_sync_manager_test.dart index 443609a..6a97eda 100644 --- a/test/unit/account_sync_manager_test.dart +++ b/test/unit/account_sync_manager_test.dart @@ -9,6 +9,7 @@ import 'package:sharedinbox/core/models/mailbox.dart'; import 'package:sharedinbox/core/repositories/account_repository.dart'; import 'package:sharedinbox/core/repositories/email_repository.dart'; import 'package:sharedinbox/core/repositories/mailbox_repository.dart'; +import 'package:sharedinbox/core/repositories/sync_log_repository.dart'; import 'package:sharedinbox/core/sync/account_sync_manager.dart'; // ── Fakes ───────────────────────────────────────────────────────────────────── @@ -184,6 +185,45 @@ class FailingJmapEmailRepository extends FakeEmailRepository { } } +class _CapturingSyncLogRepository implements SyncLogRepository { + final List entries = []; + + @override + Future log({ + required String accountId, + required bool success, + String? errorMessage, + required String protocol, + required int emailsFetched, + required int emailsSkipped, + required int mailboxesSynced, + required int pendingFlushed, + required int bytesTransferred, + required DateTime startedAt, + required DateTime finishedAt, + }) async { + entries.add( + SyncLogEntry( + id: entries.length, + result: success ? 'ok' : 'error', + errorMessage: errorMessage, + protocol: protocol, + emailsFetched: emailsFetched, + emailsSkipped: emailsSkipped, + mailboxesSynced: mailboxesSynced, + pendingFlushed: pendingFlushed, + bytesTransferred: bytesTransferred, + startedAt: startedAt, + finishedAt: finishedAt, + ), + ); + } + + @override + Stream> observeSyncLogs(String accountId) => + Stream.value(List.of(entries)); +} + // ── Tests ───────────────────────────────────────────────────────────────────── void main() { @@ -349,5 +389,57 @@ void main() { async.flushMicrotasks(); }); }); + + group('sync errors are visible in the sync log', () { + test('IMAP sync failure writes an error entry to the sync log', () { + fakeAsync((async) { + final accounts = FakeAccountRepository(); + final syncLog = _CapturingSyncLogRepository(); + final mgr = AccountSyncManager( + accounts, + FailingMailboxRepository(), + FakeEmailRepository(), + syncLog: syncLog, + ); + mgr.start(); + accounts.push([_account]); + async.flushMicrotasks(); + + expect(syncLog.entries, hasLength(1)); + expect(syncLog.entries.first.isOk, isFalse); + expect(syncLog.entries.first.errorMessage, isNotEmpty); + expect(syncLog.entries.first.protocol, 'imap'); + + mgr.dispose(); + async.elapse(const Duration(seconds: 10)); + async.flushMicrotasks(); + }); + }); + + test('JMAP sync failure writes an error entry to the sync log', () { + fakeAsync((async) { + final accounts = FakeAccountRepository(); + final syncLog = _CapturingSyncLogRepository(); + final mgr = AccountSyncManager( + accounts, + FakeMailboxRepositoryWithInbox(), + FailingJmapEmailRepository(), + syncLog: syncLog, + ); + mgr.start(); + accounts.push([_jmapAccount]); + async.flushMicrotasks(); + + expect(syncLog.entries, hasLength(1)); + expect(syncLog.entries.first.isOk, isFalse); + expect(syncLog.entries.first.errorMessage, isNotEmpty); + expect(syncLog.entries.first.protocol, 'jmap'); + + mgr.dispose(); + async.elapse(const Duration(seconds: 10)); + async.flushMicrotasks(); + }); + }); + }); }); }