mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:07:15 +01:00
`crdt_sync` is pure Dart so it can run from CLI tools and headless systemd jobs, which means it takes closures instead of depending on flutter_secure_storage and google_sign_in. Every Flutter app then wrote the same ~390 lines to supply them, and five apps ended up carrying near-identical copies of firebase_backend.dart. Those copies drifted. `todo` gained an opt-out flag and a seeded-session path that home_inventory and wake_alarm never received, so the same bug was fixed in one copy and left live in the others -- which is the actual cost being paid here, not the line count. This package owns that glue once: the keystore adapter, the persisted per-install device id, the account store and a one-call bootstrap. A new app needs a SyncApp const and `openSync(kApp)`. The wrapper routes are a parameter rather than a constant, because todo's copy hardcoded its own `~/.config/todo/firebase_auth.json`. An extraction only one app could adopt would be worse than the copies. Also documents what made this hard to find: crdt_sync_dart's README covered only the *old* GitHub transport and never mentioned the Firebase surface at all, and sync_settings_ui -- a complete drop-in settings screen -- had no README whatsoever. The five existing apps stay on their copies and on crdt_sync_dart-v0.10.0. Migration is forward-only and those fallbacks are load-bearing; converging them is a separate task, and the README says so. 35 tests, 100% line coverage, no suppressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
77 lines
2.7 KiB
Dart
77 lines
2.7 KiB
Dart
import 'package:crdt_sync_flutter/crdt_sync_flutter.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
test('generates a uuid on first call and persists it', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
final first = await loadDeviceIdentity(preferences: prefs);
|
|
|
|
expect(first.deviceId, isNotEmpty);
|
|
expect(prefs.getString(kNodeIdKey), first.deviceId);
|
|
});
|
|
|
|
test('returns the same id on every later call', () async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
final first = await loadDeviceIdentity(preferences: prefs);
|
|
final second = await loadDeviceIdentity(preferences: prefs);
|
|
|
|
// A changing id would push under a new path every launch and re-merge
|
|
// this device's own history as though a peer had written it.
|
|
expect(second.deviceId, first.deviceId);
|
|
});
|
|
|
|
test('gives two installs different ids', () async {
|
|
final a = await loadDeviceIdentity(
|
|
preferences: await SharedPreferences.getInstance(),
|
|
);
|
|
SharedPreferences.setMockInitialValues({});
|
|
final b = await loadDeviceIdentity(
|
|
preferences: await SharedPreferences.getInstance(),
|
|
);
|
|
|
|
// Two devices sharing an id overwrite each other's pushed file on every
|
|
// tick, which is the whole reason this is a uuid and not a role constant.
|
|
expect(b.deviceId, isNot(a.deviceId));
|
|
});
|
|
|
|
test('carries a legacy id so a migrated device knows itself', () async {
|
|
final identity = await loadDeviceIdentity(
|
|
legacyId: 'phone',
|
|
preferences: await SharedPreferences.getInstance(),
|
|
);
|
|
|
|
expect(identity.isOwn('phone'), isTrue);
|
|
expect(identity.isOwn(identity.deviceId), isTrue);
|
|
expect(identity.isOwn('some-other-device'), isFalse);
|
|
});
|
|
|
|
test('resolves SharedPreferences itself when none is passed', () async {
|
|
// The production call site passes nothing; the mock binding stands in for
|
|
// the real plugin, so the default path is exercised rather than assumed.
|
|
SharedPreferences.setMockInitialValues({kNodeIdKey: 'ambient-uuid'});
|
|
|
|
final identity = await loadDeviceIdentity();
|
|
|
|
expect(identity.deviceId, 'ambient-uuid');
|
|
});
|
|
|
|
test('adopts an id already written by a pre-package app', () async {
|
|
// The key matches what the existing apps wrote, so adopting this package
|
|
// keeps a device's identity instead of making it look brand new.
|
|
SharedPreferences.setMockInitialValues({kNodeIdKey: 'existing-uuid'});
|
|
|
|
final identity = await loadDeviceIdentity(
|
|
preferences: await SharedPreferences.getInstance(),
|
|
);
|
|
|
|
expect(identity.deviceId, 'existing-uuid');
|
|
});
|
|
}
|