mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 15:23:16 +02:00
M3 – GitHub OAuth device flow: replace PAT-paste with a guided "Connect
GitHub" button that runs the device-code flow; tapping with no client id
now opens a setup dialog (instructions + inline paste field) rather than
a buried inline hint. Bakes in the app's own OAuth App client id so fresh
installs work with zero manual config. Auto-syncs immediately after
connect. Verified end-to-end on the real phone: OAuth flow → token saved
→ PC's 48-entry log merged in (confirmed via food-bank vs manual source
labels in History).
M4 – Background meal-slot notifications: WorkManager periodic task (15 min
floor) checks for overdue slots and posts/cancels notifications via
flutter_local_notifications. New permissions: POST_NOTIFICATIONS,
WAKE_LOCK, RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
INTERNET (was missing — latent sync bug). "Disable battery optimization"
button in Settings. Verified on real phone: WorkManager registered, forced
run posted a real notification ("Meal not logged / You haven't logged your
16:00 meal yet."), isolated to background path (only caller is the
WorkManager dispatcher, not any foreground lifecycle hook).
AGP9 release crash fix: AGP 9 defaults isMinifyEnabled/isShrinkResources
to true for release even with no proguard config; R8 stripped
WorkDatabase_Impl's reflection-only constructor, crashing every launch
with NoSuchMethodException. Explicitly disabled both flags in
build.gradle.kts. Verified via dexdump (constructor present) and on-device
launch (no crash). Proper R8 keep rules are the long-term fix; tracked.
177 tests, flutter analyze clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SWPUBzE24Ls9i9GMRwXnnn
88 lines
2.8 KiB
Dart
88 lines
2.8 KiB
Dart
// `checkAndNotify` is the unit-testable half of the WorkManager periodic
|
|
// check; `backgroundCheckCallbackDispatcher` itself is integration-only
|
|
// (real WorkManager isolate, manual on-device smoke test) per the project
|
|
// plan, and is excluded from coverage.
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:diet_guard_app/models/nutrition.dart';
|
|
import 'package:diet_guard_app/services/background_check_service.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:diet_guard_app/services/notification_service.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../fake_notifications.dart';
|
|
|
|
const _manual = Nutrition(
|
|
kcal: 200,
|
|
proteinG: 10,
|
|
carbsG: 20,
|
|
fatG: 5,
|
|
grams: 100,
|
|
source: 'manual',
|
|
);
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
late Directory tempDir;
|
|
late List<MethodCall> notificationLog;
|
|
|
|
setUp(() async {
|
|
tempDir = await Directory.systemTemp.createTemp('diet_guard_bg_check_');
|
|
LogStorageService.resetForTesting(testDir: tempDir);
|
|
notificationLog = installFakeAndroidNotifications();
|
|
NotificationService.resetForTesting(
|
|
plugin: FlutterLocalNotificationsPlugin(),
|
|
);
|
|
});
|
|
|
|
tearDown(() async {
|
|
LogStorageService.resetForTesting();
|
|
NotificationService.resetForTesting();
|
|
await tempDir.delete(recursive: true);
|
|
});
|
|
|
|
test(
|
|
'shows due-and-unlogged slots, cancels logged and upcoming ones',
|
|
() async {
|
|
await LogStorageService.instance.logMeal('lunch', _manual, slot: 12);
|
|
|
|
await checkAndNotify(now: DateTime(2026, 1, 1, 16));
|
|
|
|
final shown = notificationLog
|
|
.where((c) => c.method == 'show')
|
|
.map((c) => (c.arguments as Map)['id'])
|
|
.toSet();
|
|
final cancelled = notificationLog
|
|
.where((c) => c.method == 'cancel')
|
|
.map((c) => (c.arguments as Map)['id'])
|
|
.toSet();
|
|
expect(shown, {8, 16});
|
|
expect(cancelled, {12, 20});
|
|
},
|
|
);
|
|
|
|
test('cancels everything when every due slot is logged', () async {
|
|
await LogStorageService.instance.logMeal('breakfast', _manual, slot: 8);
|
|
|
|
await checkAndNotify(now: DateTime(2026, 1, 1, 8));
|
|
|
|
expect(notificationLog.where((c) => c.method == 'show'), isEmpty);
|
|
expect(notificationLog.where((c) => c.method == 'cancel'), hasLength(4));
|
|
});
|
|
|
|
test('uses the real clock when now is omitted', () async {
|
|
// Just exercises the `now ?? DateTime.now()` branch without asserting
|
|
// on specific slots (which depend on the actual time the test runs).
|
|
await checkAndNotify();
|
|
expect(
|
|
notificationLog.where(
|
|
(c) => c.method == 'show' || c.method == 'cancel',
|
|
),
|
|
isNotEmpty,
|
|
);
|
|
});
|
|
}
|