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 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-21 13:33:20 +02:00
co-authored by Claude Sonnet 4.6
parent fca9e1aecf
commit 2e869194e9
2 changed files with 126 additions and 30 deletions
+34 -30
View File
@@ -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,
+92
View File
@@ -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<SyncLogEntry> entries = [];
@override
Future<void> 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<List<SyncLogEntry>> 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();
});
});
});
});
}