diet-guard/app/test/repo_invariants_test.dart
Krzysztof kuhy Rudnicki b3aeb4f4b7
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Re-point paths for the ~ reorganisation (home-tidy)
Also bumps Flutter 3.47.3, flutter_secure_storage 11.1.0, idb_shim 2.9.9,
mcp 2.2.0 and ruff 0.16.7 so the freshness gate lets this through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 19:53:03 +02:00

180 lines
7.0 KiB
Dart

/// Structural rules that used to live only as prose in CLAUDE.md.
///
/// Each rule here was previously a paragraph asking a future reader not to do
/// something. A paragraph does not fail a commit, so these are tests instead:
/// the rule and its enforcement are the same artifact, and the *why* lives in
/// the test name rather than in a document that can drift from the code.
library;
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
/// `app/` from the repo root, resolved relative to the package directory so
/// the test works regardless of the working directory `flutter test` uses.
Directory get _appDir {
var dir = Directory.current;
while (!File('${dir.path}/pubspec.yaml').existsSync()) {
final parent = dir.parent;
if (parent.path == dir.path) {
fail('could not locate app/ from ${Directory.current.path}');
}
dir = parent;
}
return dir;
}
List<File> _dartFilesIn(String relative) {
final dir = Directory('${_appDir.path}/$relative');
if (!dir.existsSync()) return const [];
return dir
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.dart'))
.toList();
}
void main() {
group('the desktop target is a web build, not a GTK embedder', () {
// Flutter's Linux embedder manages ~20fps at 3840x2160 where the same Dart
// in Chrome sustains ~144fps. `flutter create --platforms linux` would
// silently restore the slow path, and nothing about the resulting tree
// looks wrong at a glance -- so the absence of the directory is asserted.
test('app/linux/ does not exist', () {
expect(
Directory('${_appDir.path}/linux').existsSync(),
isFalse,
reason: 'A Linux embedder target was re-added. The desktop app is the '
'web build served by bin/diet_guard_desktop.dart; see CLAUDE.md.',
);
});
});
group('dart:io stays behind the platform seam', () {
// dart:io does not fail a web *compile* -- it becomes a stub that throws at
// runtime, so the symptom of a bad import is a blank white window rather
// than a build error. Widget tests run on the VM and would not catch it.
test('only *_io.dart halves and lib/desktop/ import dart:io', () {
final offenders = <String>[];
for (final file in _dartFilesIn('lib')) {
final relative = file.path.split('/app/lib/').last;
final isSeam = relative.endsWith('_io.dart');
final isDesktopOnly = relative.startsWith('desktop/');
if (isSeam || isDesktopOnly) continue;
if (file.readAsStringSync().contains("import 'dart:io'")) {
offenders.add(relative);
}
}
expect(
offenders,
isEmpty,
reason: 'These files import dart:io outside the conditional-export '
'seam, which compiles for web and throws at runtime (blank white '
'window). Move the platform edge into a *_io.dart / *_web.dart '
'pair, and branch on kIsWeb before any Platform.is… check.',
);
});
});
group('deliberately removed features stay removed', () {
// The meal builder, "repeat last meal" and the temptation-bundling reward
// prompt were each removed as unused. Re-adding one is a decision, not an
// accident -- this test exists so it cannot happen by accident.
test('no meal builder, repeat-last-meal or reward prompt', () {
final banned = RegExp(
r'MealBuilder|mealBuilder|repeatLastMeal|rewardPrompt|temptationBundl',
);
final offenders = <String>[];
for (final file in _dartFilesIn('lib')) {
if (banned.hasMatch(file.readAsStringSync())) {
offenders.add(file.path.split('/app/lib/').last);
}
}
expect(
offenders,
isEmpty,
reason: 'A removed feature came back. See the "Do NOT" section of '
'CLAUDE.md before re-adding any of these.',
);
});
// Photos took image_picker, the blob stores, the wrapper's /blobs/ route
// and the CAMERA permission with them. Re-adding photos means re-adding
// all of that *plus* syncing image blobs, since a device-local attachment
// would violate the one-source-of-truth rule.
test('no image_picker dependency', () {
final pubspec = File('${_appDir.path}/pubspec.yaml').readAsStringSync();
expect(
pubspec.contains('image_picker'),
isFalse,
reason: 'Meal photos were removed deliberately. A device-local image '
'attachment also breaks the shared-source-of-truth rule.',
);
});
test('no CAMERA permission in the Android manifest', () {
final manifest = File(
'${_appDir.path}/android/app/src/main/AndroidManifest.xml',
);
expect(
manifest.readAsStringSync().contains('android.permission.CAMERA'),
isFalse,
reason: 'The CAMERA permission came back with no photo feature to '
'justify it.',
);
});
});
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
// behind a different origin, which reads as data loss to the user.
test('the wrapper port is still 8732', () {
final source = File(
'${_appDir.path}/lib/services/desktop_wrapper.dart',
).readAsStringSync();
expect(
source.contains('desktopWrapperPort = 8732'),
isTrue,
reason: 'The wrapper port changed. IndexedDB is keyed by origin, so a '
'new port looks like a different app with no history at all. '
'8730 is ~/src/todo and 8731 is ~/src/habit_stack; do not collide.',
);
});
test('the Chrome profile directory is still diet-guard-desktop', () {
final source = File(
'${_appDir.path}/bin/diet_guard_desktop.dart',
).readAsStringSync();
expect(
source.contains("'diet-guard-desktop', 'profile'"),
isTrue,
reason: 'The Chrome --user-data-dir moved. IndexedDB lives in that '
'profile, so relocating it hides the entire local food log.',
);
});
});
}