mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later day to mark it sick, tap again to clear; past days inert. Writes the sickdays wire format (state sick/cleared + actor, one HLC per write) to sickdays-sync/devices via crdt_sync syncLog. - Mutual exclusion in both pickers: each syncs the other pool on open and refuses a date it holds, naming the pool; an unreadable pool refuses all. - Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the ring with reason "sick_day" (no markFired, no ring_started); StickCheck verdict sick_day, streak skips sick days; stakes card says so. - Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request code 1006) runs a headless engine (poolSyncMain) that pulls both pools from RTDB and rewrites both native caches, so a PC mark arrives with the app closed. The ring decision stays a cache read. - JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision, PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check. pylint hook skipped: verify.py imports sickdays, not in the hook env until the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
119 lines
4.0 KiB
Dart
119 lines
4.0 KiB
Dart
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:wake_alarm_sync/services/alarm_scheduler.dart';
|
|
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
|
import 'package:wake_alarm_sync/services/sick_day_sync.dart';
|
|
|
|
import '../fake_day_pool_store.dart';
|
|
|
|
final _today = DateTime(2026, 10, 16);
|
|
bool _never(DateTime _) => false;
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late List<MethodCall> calls;
|
|
late AlarmScheduler scheduler;
|
|
var channelThrows = false;
|
|
|
|
setUp(() {
|
|
calls = <MethodCall>[];
|
|
channelThrows = false;
|
|
const channel = MethodChannel(AlarmScheduler.channelName);
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(channel, (call) async {
|
|
calls.add(call);
|
|
if (channelThrows) throw PlatformException(code: 'no_platform');
|
|
return null;
|
|
});
|
|
scheduler = AlarmScheduler(channel: channel);
|
|
});
|
|
|
|
Future<SickDayPool> openPool() =>
|
|
SickDayPool.open(persistence: MemPersistence(), deviceId: 'phone');
|
|
|
|
List<String> lastCache() {
|
|
expect(calls.last.method, 'setSickDays');
|
|
return ((calls.last.arguments as Map)['dates'] as List).cast<String>();
|
|
}
|
|
|
|
test("pulls the PC's sick day and pushes the phone's own", () async {
|
|
final remote = MemRemote();
|
|
remote.files['${SickDayPool.pathPrefix}/$pythonDeviceId/'
|
|
'${SickDayPool.filename}'] =
|
|
pythonSickLog;
|
|
final pool = await openPool();
|
|
await pool.add(DateTime(2026, 10, 23), isFree: _never, now: _today);
|
|
|
|
final sync = SickDaySync(
|
|
openClient: () async => remote,
|
|
scheduler: scheduler,
|
|
);
|
|
expect(await sync.sync(pool), 'Synced ✓');
|
|
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isTrue, reason: 'pulled');
|
|
expect(pool.isSick(DateTime(2026, 10, 21)), isFalse, reason: 'cleared');
|
|
final pushed = remote.files['sickdays-sync/devices/phone/sick_days.json'];
|
|
expect(pushed, contains('"2026-10-23"'));
|
|
expect(lastCache(), ['2026-10-20', '2026-10-23']);
|
|
});
|
|
|
|
test('a later clear on the phone beats an earlier PC mark', () async {
|
|
final remote = MemRemote();
|
|
remote.files['${SickDayPool.pathPrefix}/$pythonDeviceId/'
|
|
'${SickDayPool.filename}'] =
|
|
pythonSickLog;
|
|
final pool = await openPool();
|
|
final sync = SickDaySync(
|
|
openClient: () async => remote,
|
|
scheduler: scheduler,
|
|
);
|
|
await sync.sync(pool);
|
|
await pool.remove(DateTime(2026, 10, 20), now: _today);
|
|
await sync.sync(pool);
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isFalse);
|
|
expect(lastCache(), isEmpty);
|
|
});
|
|
|
|
test('not connected to Firebase still refreshes the cache', () async {
|
|
final pool = await openPool();
|
|
await pool.add(_today, isFree: _never, now: _today);
|
|
final sync = SickDaySync(
|
|
openClient: () async => null,
|
|
scheduler: scheduler,
|
|
);
|
|
expect(await sync.sync(pool), contains('connect Firebase'));
|
|
expect(lastCache(), ['2026-10-16']);
|
|
});
|
|
|
|
test('an unreachable remote reports an error and keeps the cache', () async {
|
|
final pool = await openPool();
|
|
await pool.add(_today, isFree: _never, now: _today);
|
|
final sync = SickDaySync(
|
|
openClient: () async => BrokenRemote(),
|
|
scheduler: scheduler,
|
|
);
|
|
expect(await sync.sync(pool), startsWith('Error:'));
|
|
expect(lastCache(), ['2026-10-16']);
|
|
});
|
|
|
|
test('a client factory that throws is reported, not propagated', () async {
|
|
final pool = await openPool();
|
|
final sync = SickDaySync(
|
|
openClient: () async => throw StateError('keystore locked'),
|
|
scheduler: scheduler,
|
|
);
|
|
expect(await sync.sync(pool), contains('keystore locked'));
|
|
expect(lastCache(), isEmpty);
|
|
});
|
|
|
|
test('a missing platform channel does not fail the sync', () async {
|
|
channelThrows = true;
|
|
final sync = SickDaySync(
|
|
openClient: () async => MemRemote(),
|
|
scheduler: scheduler,
|
|
);
|
|
expect(await sync.sync(await openPool()), 'Synced ✓');
|
|
});
|
|
}
|