diff --git a/app/lib/main.dart b/app/lib/main.dart index e5967dd..85accc0 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -10,6 +10,7 @@ import 'package:diet_guard_app/services/budget_history_service.dart'; import 'package:diet_guard_app/services/foodbank_service.dart'; import 'package:diet_guard_app/services/frame_stats.dart'; import 'package:diet_guard_app/services/kuchnia_credential_service.dart'; +import 'package:diet_guard_app/services/kuchnia_queue.dart'; import 'package:diet_guard_app/services/log_storage_service.dart'; import 'package:diet_guard_app/services/meal_schedule_service.dart'; import 'package:diet_guard_app/services/notification_service.dart'; @@ -28,6 +29,7 @@ Future main() async { await MealScheduleService.init(); await FoodBankService.init(); await KuchniaCredentialService.init(); + await KuchniaQueueService.init(); final notifications = await NotificationService.init(); // Deliberately *not* awaited before the first frame: the browser's // `Notification.requestPermission()` does not complete until the user diff --git a/app/lib/services/kuchnia_queue.dart b/app/lib/services/kuchnia_queue.dart index be15cc0..197212b 100644 --- a/app/lib/services/kuchnia_queue.dart +++ b/app/lib/services/kuchnia_queue.dart @@ -23,6 +23,7 @@ library; import 'package:diet_guard_app/models/kuchnia_dish.dart'; import 'package:diet_guard_app/services/document_store.dart'; +import 'package:diet_guard_app/services/document_store_factory.dart'; import 'package:diet_guard_app/services/kuchnia_import.dart'; import 'package:diet_guard_app/services/kuchnia_orders.dart' show isoDay; import 'package:flutter/foundation.dart'; @@ -60,7 +61,24 @@ class KuchniaQueueService { /// How many dishes are still waiting to be logged. static int get remaining => _instance?._pending.length ?? 0; - /// Initialises from [store], for tests and for [init]. + /// Initialises the singleton against the platform document store. + /// + /// Without this the queue silently no-ops on a real device: every entry + /// point checks [isInitialized] first, so a missing call reads as "no + /// delivery" rather than as an error. + static Future init() async { + if (_instance != null) return _instance!; + // Resolving the platform store is a plugin call, not reachable from + // `flutter test`. + // coverage:ignore-start + final service = KuchniaQueueService._(await openDocumentStore()); + // coverage:ignore-end + await service._load(); + _instance = service; + return service; + } + + /// Initialises from [store], for tests. @visibleForTesting static Future initForTesting(DocumentStore store) async { final service = KuchniaQueueService._(store); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5a58530..89fd722 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: diet_guard_app description: "Companion phone app for diet_guard: log meals on the go." publish_to: 'none' -version: 1.0.64+1 +version: 1.0.65+1 environment: sdk: ^3.12.2 diff --git a/app/test/repo_invariants_test.dart b/app/test/repo_invariants_test.dart index e0c40e7..b025eb0 100644 --- a/app/test/repo_invariants_test.dart +++ b/app/test/repo_invariants_test.dart @@ -125,6 +125,28 @@ void main() { }); }); + group('singletons that silently no-op are initialised at startup', () { + // Every catering entry point checks `isInitialized` first and returns + // quietly when it is false, so a missing `init()` does not throw -- it + // reads as "no delivery" and "no credential" forever. That is invisible in + // widget tests, which initialise these directly, and was caught only by + // grepping main.dart before a phone deploy. + test('main.dart initialises the catering services', () { + final source = File('${_appDir.path}/lib/main.dart').readAsStringSync(); + for (final call in [ + 'KuchniaCredentialService.init()', + 'KuchniaQueueService.init()', + ]) { + expect( + source.contains(call), + isTrue, + reason: '$call is missing from main(). The feature will silently do ' + 'nothing on a real device while every test still passes.', + ); + } + }); + }); + group('the desktop wrapper origin is fixed', () { // IndexedDB is keyed by origin and lives in the Chrome profile. Changing // either the port or the --user-data-dir hides the entire local food log