mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 14:48:45 +01:00
Some checks failed
Meals logged on the phone stopped reaching the PC, so the gate kept locking for slots that were already logged and the only way through was to spam "nothing". The PC's fetch was never broken: it was faithfully pulling a Firebase that did not contain the phone's meals. The phone had no working publish backstop on either transport: - `checkAndNotify` gated its sync on `due.isNotEmpty`. Logging every meal promptly means nothing is ever due, so the ~15-min periodic task never published. It was a notification job that happened to sync, not a sync timer. It now syncs on every tick it is asked to, then computes due slots from the merged result, so a meal pulled from another device suppresses a nag instead of triggering one. - `backgroundSyncPush` never called `initSyncDeviceId()`. A fresh isolate has its own static state, so `currentSyncDeviceId` fell back to the compile-time 'phone' constant and published to `devices/phone/` rather than the persisted uuid -- which then makes the next foreground push see `unchanged` and skip writing the uuid directory entirely. Separately, on the "Fetch from sync" path itself: `FirebaseSyncError` and `FirebaseAuthError` are siblings of `GitHubSyncError` under `RemoteSyncError`, not subclasses, so every per-request failure of the *primary* backend escaped `pull_shared_log`'s catch tuple and raised a traceback out of a helper documented to fail closed with a reason string. `ConfigError` needed the same treatment by a different route: it subclasses `Exception` directly, so it is outside `RemoteSyncError` too, and is now translated at `_client_for_run`. The PAT is no longer mandatory either. Firebase is primary, but `_read_token` ran before the client was ever built, so a Firebase-only machine could not sync at all -- the commits that fixed this touched only the Dart side. A stalled sync was invisible: the config gates returned silently and `openFirebase` swallowed every error. `dart:developer`'s `log()` does not reach logcat on a release build (verified on device), so logging alone would not have surfaced it. `SyncHealth` records each tick's outcome and the log screen shows a banner when publishing has actually stopped. Also make the real-Tk probes bypass the window manager. A tiling WM retiles a managed toplevel instead of honouring the requested geometry, so `winfo_height` reported 0/1px and the layout and focus-ring assertions failed on noise rather than on any regression -- 8 failures on the dev machine that only passed under Xvfb. `overrideredirect` makes the measurement deterministic with or without a WM; the probes still detect a genuine overflow at 1366x300. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RT8X32S9yDLW1Xf1PDq8z7
135 lines
4.6 KiB
Dart
135 lines
4.6 KiB
Dart
import 'package:diet_guard_app/services/sync_health.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
group('SyncHealthStatus.isStalled', () {
|
|
test('a device that has never synced is not flagged', () {
|
|
// A fresh install with no account yet is normal, not a fault; nagging
|
|
// there would train the banner to be ignored.
|
|
final fresh = SyncHealthStatus(
|
|
lastSuccess: null,
|
|
failureKind: null,
|
|
now: DateTime(2026, 8, 10),
|
|
);
|
|
expect(fresh.isStalled, isFalse);
|
|
expect(fresh.message, isNull);
|
|
});
|
|
|
|
test('a recent success is healthy', () {
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: now.subtract(const Duration(minutes: 30)),
|
|
failureKind: null,
|
|
now: now,
|
|
);
|
|
expect(status.isStalled, isFalse);
|
|
});
|
|
|
|
test('a success older than the staleness window is flagged', () {
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: now.subtract(kSyncStaleAfter + const Duration(hours: 1)),
|
|
failureKind: null,
|
|
now: now,
|
|
);
|
|
expect(status.isStalled, isTrue);
|
|
expect(status.message, contains('Not syncing since'));
|
|
});
|
|
|
|
test('an outright failure is flagged even when recent', () {
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: now.subtract(const Duration(minutes: 1)),
|
|
failureKind: SyncFailureKind.failed,
|
|
now: now,
|
|
);
|
|
expect(status.isStalled, isTrue);
|
|
});
|
|
|
|
test('an unconfigured device names the missing account', () {
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: now.subtract(const Duration(days: 2)),
|
|
failureKind: SyncFailureKind.unconfigured,
|
|
now: now,
|
|
);
|
|
expect(status.message, contains('no account connected'));
|
|
});
|
|
|
|
test('reports days for a long stall and hours for a short one', () {
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
String? messageAfter(Duration since) => SyncHealthStatus(
|
|
lastSuccess: now.subtract(since),
|
|
failureKind: null,
|
|
now: now,
|
|
).message;
|
|
|
|
expect(messageAfter(const Duration(days: 3)), contains('3d ago'));
|
|
expect(messageAfter(const Duration(hours: 9)), contains('9h ago'));
|
|
});
|
|
|
|
test('a failure just after a success does not say "0h ago"', () {
|
|
// Regression guard: reporting elapsed time before any has elapsed
|
|
// rendered "Not syncing since 0h ago", which reads as a bug in the
|
|
// banner rather than a report about the sync.
|
|
final now = DateTime(2026, 8, 10, 12);
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: now.subtract(const Duration(minutes: 5)),
|
|
failureKind: SyncFailureKind.failed,
|
|
now: now,
|
|
);
|
|
expect(status.isStalled, isTrue);
|
|
expect(status.message, 'Not syncing. Meals stay on this device.');
|
|
expect(status.message, isNot(contains('0h')));
|
|
});
|
|
|
|
test('a failure with no prior success still reads sensibly', () {
|
|
final status = SyncHealthStatus(
|
|
lastSuccess: null,
|
|
failureKind: SyncFailureKind.failed,
|
|
now: DateTime(2026, 8, 10),
|
|
);
|
|
expect(status.message, 'Not syncing. Meals stay on this device.');
|
|
});
|
|
});
|
|
|
|
group('SyncHealth persistence', () {
|
|
test('recordSuccess clears a previous failure', () async {
|
|
await SyncHealth.recordFailure();
|
|
expect((await SyncHealth.read()).failureKind, SyncFailureKind.failed);
|
|
|
|
await SyncHealth.recordSuccess();
|
|
final status = await SyncHealth.read();
|
|
expect(status.failureKind, isNull);
|
|
expect(status.lastSuccess, isNotNull);
|
|
expect(status.isStalled, isFalse);
|
|
});
|
|
|
|
test('recordUnconfigured round-trips', () async {
|
|
await SyncHealth.recordUnconfigured();
|
|
expect(
|
|
(await SyncHealth.read()).failureKind,
|
|
SyncFailureKind.unconfigured,
|
|
);
|
|
});
|
|
|
|
test('an unrecognised stored kind reads as no failure', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'sync.health.failureKind': 'not-a-kind',
|
|
});
|
|
expect((await SyncHealth.read()).failureKind, isNull);
|
|
});
|
|
|
|
test('resetForTest forgets everything', () async {
|
|
await SyncHealth.recordSuccess();
|
|
await SyncHealth.resetForTest();
|
|
final status = await SyncHealth.read();
|
|
expect(status.lastSuccess, isNull);
|
|
expect(status.failureKind, isNull);
|
|
});
|
|
});
|
|
}
|