mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 19:33:55 +01:00
Tenth batch of the 250-line sweep (over-cap count 18 -> 12; all Python is
already clear, so the remainder is Dart).
sync_merge_test.dart 694 -> 232 (+ 4 siblings)
github_mirror_screen_test.dart 512 -> 250 (+ 3 siblings)
foodbank_service_test.dart 395 -> 190 (+ manual sibling)
sync_service_test.dart 394 -> 203 (+ budget sibling + support)
log_storage_service_test.dart 385 -> 182 (+ edit sibling)
log_meal_screen_test.dart 362 -> 243 (+ nav sibling)
sync_service_test's ~100-line fixture block is shared through
`sync_service_test_support.dart` rather than duplicated: copying it would have
put both siblings back over the cap on their own. The helpers had to become
public (`_FakeGitHub` -> `FakeGitHub`, `_manual` -> `manualNutrition`,
`_remoteBudgetJson` -> `remoteBudgetJson`) with doc comments, since a leading
underscore is library-private in Dart and the siblings could not otherwise see
them.
The block-boundary matcher earned two fixes, both caught by comparing block
counts against HEAD rather than by the suite:
- A test name wrapped onto its own line (dart format does this past 80 cols)
was invisible to a same-line-only pattern, so the neighbouring span sliced
through it -- the file stopped compiling. Spans now end at the matching
close paren of the call, not at a guess past the closure brace.
- A double-quoted name containing an apostrophe ("skips its own device id
('phone')") was excluded by a character class that banned both quote
characters. That block fell into the shared prologue and was duplicated into
every output: the suite went GREEN at 559 tests instead of 555. Reverted and
redone; text between two blocks is now treated as a shared helper.
Verified: flutter test 555 (unchanged, and each split family's block count
matches HEAD exactly), flutter analyze clean, pre-commit clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
183 lines
6.1 KiB
Dart
183 lines
6.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:diet_guard_app/models/food_entry.dart';
|
|
import 'package:diet_guard_app/models/local_time.dart';
|
|
import 'package:diet_guard_app/models/meal_component.dart';
|
|
import 'package:diet_guard_app/models/nutrition.dart';
|
|
import 'package:diet_guard_app/services/document_store_io.dart';
|
|
import 'package:diet_guard_app/services/log_storage_service.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
const _manual = Nutrition(
|
|
kcal: 150,
|
|
proteinG: 5,
|
|
carbsG: 20,
|
|
fatG: 3,
|
|
grams: 50,
|
|
source: 'manual',
|
|
);
|
|
|
|
void main() {
|
|
|
|
late Directory tempDir;
|
|
|
|
setUp(() async {
|
|
tempDir = await Directory.systemTemp.createTemp('diet_guard_test_');
|
|
LogStorageService.resetForTesting(store: FileDocumentStore(tempDir));
|
|
});
|
|
|
|
tearDown(() async {
|
|
LogStorageService.resetForTesting();
|
|
await tempDir.delete(recursive: true);
|
|
});
|
|
|
|
group('readLog', () {
|
|
test('returns an empty log when no file exists yet', () async {
|
|
expect(await LogStorageService.instance.readLog(), isEmpty);
|
|
});
|
|
|
|
test('returns an empty log for unparsable JSON', () async {
|
|
final file = File('${tempDir.path}/food_log.json');
|
|
await file.writeAsString('not json');
|
|
expect(await LogStorageService.instance.readLog(), isEmpty);
|
|
});
|
|
|
|
test('returns an empty log when the JSON root is not a map', () async {
|
|
final file = File('${tempDir.path}/food_log.json');
|
|
await file.writeAsString('[]');
|
|
expect(await LogStorageService.instance.readLog(), isEmpty);
|
|
});
|
|
|
|
test('skips a date key whose value is not a list', () async {
|
|
final file = File('${tempDir.path}/food_log.json');
|
|
await file.writeAsString('{"2026-06-22": "not a list"}');
|
|
expect(await LogStorageService.instance.readLog(), isEmpty);
|
|
});
|
|
});
|
|
group('logMeal', () {
|
|
test('assigns a fresh id and never an hmac', () async {
|
|
final entry = await LogStorageService.instance.logMeal(
|
|
'toast',
|
|
_manual,
|
|
slot: 8,
|
|
);
|
|
expect(entry.id, isNotEmpty);
|
|
expect(entry.hmac, isNull);
|
|
expect(entry.slot, 8);
|
|
expect(entry.desc, 'toast');
|
|
});
|
|
|
|
test('persists components when given', () async {
|
|
const components = [
|
|
MealComponent(
|
|
name: 'bread',
|
|
kcal: 100,
|
|
proteinG: 3,
|
|
carbsG: 18,
|
|
fatG: 1,
|
|
grams: 40,
|
|
),
|
|
];
|
|
final entry = await LogStorageService.instance.logMeal(
|
|
'toast meal',
|
|
_manual,
|
|
components: components,
|
|
);
|
|
expect(entry.components, hasLength(1));
|
|
final reloaded = await LogStorageService.instance.todayEntries();
|
|
expect(reloaded.single.components!.single.name, 'bread');
|
|
});
|
|
|
|
test("two logged meals both persist under today's date key", () async {
|
|
await LogStorageService.instance.logMeal('a', _manual);
|
|
await LogStorageService.instance.logMeal('b', _manual);
|
|
final entries = await LogStorageService.instance.todayEntries();
|
|
expect(entries, hasLength(2));
|
|
});
|
|
});
|
|
group('undoLastToday', () {
|
|
test('returns null when today has no entries', () async {
|
|
expect(await LogStorageService.instance.undoLastToday(), isNull);
|
|
});
|
|
|
|
test('tombstones the most recent entry in place', () async {
|
|
await LogStorageService.instance.logMeal('first', _manual);
|
|
final second = await LogStorageService.instance.logMeal(
|
|
'second',
|
|
_manual,
|
|
);
|
|
final undone = await LogStorageService.instance.undoLastToday();
|
|
expect(undone!.id, second.id);
|
|
expect(undone.deleted, isTrue);
|
|
});
|
|
|
|
test('tombstoned entries are excluded from todayEntries', () async {
|
|
await LogStorageService.instance.logMeal('only', _manual);
|
|
await LogStorageService.instance.undoLastToday();
|
|
expect(await LogStorageService.instance.todayEntries(), isEmpty);
|
|
});
|
|
|
|
test("never touches a previous day's entries", () async {
|
|
final yesterdayKey = localDateKey(
|
|
DateTime.now().subtract(const Duration(days: 1)),
|
|
);
|
|
final yesterday = FoodEntry(
|
|
id: 'yesterday-1',
|
|
time: '${yesterdayKey}T08:00:00+02:00',
|
|
desc: 'yesterday meal',
|
|
grams: 50,
|
|
kcal: 150,
|
|
proteinG: 5,
|
|
carbsG: 20,
|
|
fatG: 3,
|
|
source: 'manual',
|
|
);
|
|
await LogStorageService.instance.writeLog({
|
|
yesterdayKey: [yesterday],
|
|
});
|
|
await LogStorageService.instance.logMeal('today', _manual);
|
|
|
|
expect(await LogStorageService.instance.undoLastToday(), isNotNull);
|
|
|
|
final log = await LogStorageService.instance.readLog();
|
|
expect(log[yesterdayKey]!.single.deleted, isFalse);
|
|
});
|
|
|
|
test(
|
|
'skips an already-tombstoned entry and undoes the one before it',
|
|
() async {
|
|
final first = await LogStorageService.instance.logMeal(
|
|
'first',
|
|
_manual,
|
|
);
|
|
await LogStorageService.instance.logMeal('second', _manual);
|
|
await LogStorageService.instance.undoLastToday();
|
|
final undoneAgain = await LogStorageService.instance.undoLastToday();
|
|
expect(undoneAgain!.id, first.id);
|
|
expect(await LogStorageService.instance.undoLastToday(), isNull);
|
|
},
|
|
);
|
|
});
|
|
group('todayTotalKcal', () {
|
|
test("sums kcal across today's non-tombstoned entries", () async {
|
|
await LogStorageService.instance.logMeal('a', _manual);
|
|
await LogStorageService.instance.logMeal('b', _manual);
|
|
expect(await LogStorageService.instance.todayTotalKcal(), 300.0);
|
|
});
|
|
|
|
test('excludes a tombstoned entry from the total', () async {
|
|
await LogStorageService.instance.logMeal('a', _manual);
|
|
await LogStorageService.instance.logMeal('b', _manual);
|
|
await LogStorageService.instance.undoLastToday();
|
|
expect(await LogStorageService.instance.todayTotalKcal(), 150.0);
|
|
});
|
|
});
|
|
group('loggedSlotsToday', () {
|
|
test('returns only the slots with a logged entry', () async {
|
|
await LogStorageService.instance.logMeal('a', _manual, slot: 8);
|
|
await LogStorageService.instance.logMeal('b', _manual);
|
|
expect(await LogStorageService.instance.loggedSlotsToday(), {8});
|
|
});
|
|
});
|
|
}
|