mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
- FoodEntry.slot -> slotMinute: read via entrySlotMinute, written via slotFields (whole-hour stays byte-identical, else slot_min), kept by copyWithDeleted and the edit screen, so the phone never republishes a stripped copy of a PC entry. Out-of-range peer values echo, never throw. - Satisfaction is nearest-slot snapping (satisfiedSlots) everywhere: loggedSlotsToday, due check, kuchnia occupancy, future-day logged set (that day's schedule), slot selector. Schedule edits no longer un-log. - Future pre-log builds DateTime from slot ~/ 60, slot % 60. - Schedule history + sync go through scheduleToWire/scheduleFromWire. - Notifications: ids are slot minutes; syncToSlots cancels whatever the platform reports active (getActiveNotifications, reminder channel) that is not due, covering legacy hour ids; full 0..1439 sweep only if the lookup throws. - Settings: 15-minute dropdowns + "Custom..." (24h showTimePicker), off-grid values always selectable, counts capped by grid capacity; editor moved to widgets/meal_schedule_editor.dart + meal_time_dropdown.dart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015npsYVB2PdtWkG7MPAi4tn
148 lines
5.1 KiB
Dart
148 lines
5.1 KiB
Dart
/// Tests for meal-schedule persistence.
|
|
///
|
|
/// Split from `meal_schedule_history_test.dart` (250-line cap); that file
|
|
/// keeps the pure parsing/derivation half. Mirrors the storage half of
|
|
/// `diet_guard/tests/test_meal_schedule_store.py`.
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:diet_guard_app/models/meal_schedule.dart';
|
|
import 'package:diet_guard_app/services/document_store.dart';
|
|
import 'package:diet_guard_app/services/meal_schedule_history.dart';
|
|
import 'package:diet_guard_app/services/meal_schedule_service.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
/// In-memory store, so these tests need no plugin channel.
|
|
class _MemoryStore implements DocumentStore {
|
|
final Map<String, String> documents = {};
|
|
|
|
@override
|
|
Future<String?> read(String name) async => documents[name];
|
|
|
|
@override
|
|
Future<void> write(String name, String contents) async {
|
|
documents[name] = contents;
|
|
}
|
|
}
|
|
|
|
DateTime _at(String day) => DateTime.parse('${day}T12:00:00');
|
|
|
|
void main() {
|
|
late _MemoryStore store;
|
|
|
|
setUp(() {
|
|
store = _MemoryStore();
|
|
MealScheduleService.resetForTesting(store: store);
|
|
});
|
|
|
|
tearDown(MealScheduleService.resetForTesting);
|
|
|
|
group('MealScheduleService', () {
|
|
test('current is the default before any edit', () {
|
|
expect(MealScheduleService.current, kDefaultSchedule);
|
|
expect(MealScheduleService.updatedAt, isNull);
|
|
});
|
|
|
|
test('current degrades to the default when uninitialised', () {
|
|
MealScheduleService.resetForTesting();
|
|
expect(MealScheduleService.current, kDefaultSchedule);
|
|
expect(MealScheduleService.history.entries, isEmpty);
|
|
expect(MealScheduleService.isInitialized, isFalse);
|
|
});
|
|
|
|
test(
|
|
'recordChange persists, reloads, and grandfathers past days',
|
|
() async {
|
|
await MealScheduleService.initForTesting(store);
|
|
await MealScheduleService.instance.recordChange(
|
|
const MealSchedule(firstMinute: 8 * 60, lastMinute: 20 * 60, count: 5),
|
|
);
|
|
|
|
await MealScheduleService.initForTesting(store);
|
|
expect(
|
|
MealScheduleService.current,
|
|
const MealSchedule(firstMinute: 8 * 60, lastMinute: 20 * 60, count: 5),
|
|
);
|
|
// The whole point of the history: a day before the edit keeps the
|
|
// four-meal schedule it was actually judged against.
|
|
expect(
|
|
MealScheduleService.history.forDay('2020-01-01'),
|
|
kDefaultSchedule,
|
|
);
|
|
expect(MealScheduleService.updatedAt, isNotNull);
|
|
},
|
|
);
|
|
|
|
test('a corrupt document degrades to the default', () async {
|
|
store.documents[MealScheduleService.documentName] = '{not json';
|
|
await MealScheduleService.initForTesting(store);
|
|
expect(MealScheduleService.current, kDefaultSchedule);
|
|
});
|
|
|
|
test('an absent document loads as no history', () async {
|
|
await MealScheduleService.initForTesting(store);
|
|
expect(MealScheduleService.history.entries, isEmpty);
|
|
});
|
|
|
|
test(
|
|
'updatedAt stays null when the stored stamp is not a string',
|
|
() async {
|
|
store.documents[MealScheduleService.documentName] = jsonEncode({
|
|
'v': 1,
|
|
'e': <String, Object?>{},
|
|
't': 7,
|
|
});
|
|
await MealScheduleService.initForTesting(store);
|
|
expect(MealScheduleService.updatedAt, isNull);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'applyMerged replaces the history but ignores an empty merge',
|
|
() async {
|
|
await MealScheduleService.initForTesting(store);
|
|
await MealScheduleService.instance.recordChange(
|
|
const MealSchedule(firstMinute: 8 * 60, lastMinute: 20 * 60, count: 5),
|
|
);
|
|
|
|
// A pre-feature peer contributes no `sched:` fields; writing that back
|
|
// would discard this device's own history.
|
|
await MealScheduleService.instance.applyMerged(const []);
|
|
expect(
|
|
MealScheduleService.current,
|
|
const MealSchedule(firstMinute: 8 * 60, lastMinute: 20 * 60, count: 5),
|
|
);
|
|
|
|
final stamp = DateTime.parse('2026-08-16T09:00:00.000');
|
|
await MealScheduleService.instance.applyMerged([
|
|
ScheduleEntry(
|
|
effectiveFrom: kScheduleEpochDay,
|
|
schedule: const MealSchedule(firstMinute: 7 * 60, lastMinute: 19 * 60, count: 3),
|
|
editedAt: stamp.toIso8601String(),
|
|
),
|
|
], updatedAt: stamp);
|
|
expect(
|
|
MealScheduleService.current,
|
|
const MealSchedule(firstMinute: 7 * 60, lastMinute: 19 * 60, count: 3),
|
|
);
|
|
// The winner's stamp is kept verbatim, so re-syncing is idempotent.
|
|
expect(MealScheduleService.updatedAt, stamp);
|
|
},
|
|
);
|
|
|
|
test('the stored document is plain readable JSON', () async {
|
|
await MealScheduleService.initForTesting(store);
|
|
await MealScheduleService.instance.recordChange(
|
|
const MealSchedule(firstMinute: 8 * 60, lastMinute: 20 * 60, count: 5),
|
|
when: _at('2026-08-16'),
|
|
);
|
|
final decoded =
|
|
jsonDecode(store.documents[MealScheduleService.documentName]!)
|
|
as Map<String, Object?>;
|
|
expect(decoded['v'], 1);
|
|
expect((decoded['e']! as Map).keys, contains('2026-08-16'));
|
|
});
|
|
});
|
|
}
|