Files
sync_replay_project/apps/desktop/test/singletons_test.dart
T
foxyzandClaude Opus 5.5 4d00b7455e
ci / core (push) Successful in 27s
ci / server (push) Successful in 1m7s
ci / updater-windows (push) Failing after 6m4s
Restructure step 3: your server, Invite, version gate, AppGraph
- One server per app run (loopback, no password); Invite attaches an
  all-interfaces listener to the same server and Stop sharing detaches
  it (guests told with 4016); members, media and Up next never reset.
  No solo mode: you're home on your own server unless in a friend's.
- Navigation: none / yours / friend; a join passes a version gate on
  /health first (clear "they/you need to update", update offer).
- Invite as a value (host, port, name, password flag, version, fp
  reserved); addresses filtered (loopback, link-local, 198.18/15,
  bridges) and ranked; Sharing chip, Copy invite, Stop sharing.
- Alone looks like a plain player (alone replaces solo).
- AppGraph builds and disposes systems; widgets use AppScope;
  SessionState deleted; a test caps the remaining singletons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:52:52 +03:00

48 lines
1.7 KiB
Dart

import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
/// The app's own `.instance` singletons and how often `lib` reaches for
/// each. Systems get their dependencies through the composition root
/// (`AppGraph`, widgets through `AppScope`); this list may only shrink.
/// Lower a number when a use goes; never raise one or add a class.
const baseline = {
'AppSettings': 55,
'JellyfinAccounts': 10,
'UpdateController': 8,
'Profile': 8,
'FolderStore': 6,
'AppYtDlp': 4,
'RecentLinks': 3,
};
void main() {
test('no new .instance singletons, and no more uses of the old ones', () {
final files = Directory('lib')
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.dart'))
.toList();
final sources = [for (final f in files) f.readAsStringSync()];
// The app's own classes (framework singletons like
// `WidgetsBinding.instance` are not counted).
final classes = {
for (final s in sources)
for (final m in RegExp(r'^(?:abstract |final |base )*class (\w+)', multiLine: true).allMatches(s)) m.group(1)!,
};
final counts = <String, int>{};
for (final s in sources) {
for (final m in RegExp(r'\b([A-Z]\w*)\.instance\b').allMatches(s)) {
final name = m.group(1)!;
if (classes.contains(name)) counts[name] = (counts[name] ?? 0) + 1;
}
}
final grown = {
for (final e in counts.entries)
if (e.value > (baseline[e.key] ?? 0)) e.key: '${e.value} (baseline ${baseline[e.key] ?? 'none'})',
};
expect(grown, isEmpty,
reason: 'pass the system in through AppGraph / AppScope instead of a new `.instance` use or singleton');
});
}