mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19:27:02 +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
106 lines
3.6 KiB
Dart
106 lines
3.6 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/day_pool_refresh.dart';
|
|
import 'package:wake_alarm_sync/services/free_day_pool.dart';
|
|
import 'package:wake_alarm_sync/services/free_day_sync.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';
|
|
|
|
/// The PC→phone path with the app closed: what `poolSyncMain` runs.
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late Map<String, List<String>> caches;
|
|
late AlarmScheduler scheduler;
|
|
late List<String> done;
|
|
const poolChannel = MethodChannel(poolSyncChannelName);
|
|
|
|
setUp(() {
|
|
caches = {};
|
|
done = [];
|
|
const channel = MethodChannel(AlarmScheduler.channelName);
|
|
final messenger =
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
..setMockMethodCallHandler(channel, (call) async {
|
|
caches[call.method] = ((call.arguments as Map)['dates'] as List)
|
|
.cast<String>();
|
|
return null;
|
|
})
|
|
..setMockMethodCallHandler(poolChannel, (call) async {
|
|
done.add(call.method);
|
|
return null;
|
|
});
|
|
addTearDown(() => messenger.setMockMethodCallHandler(poolChannel, null));
|
|
scheduler = AlarmScheduler(channel: channel);
|
|
});
|
|
|
|
test('a sick day the PC pushed to RTDB reaches the native cache', () async {
|
|
// What `sickdays add 2026-10-20` + the PC's sync timer leave in RTDB.
|
|
final remote = MemRemote();
|
|
remote.files['sickdays-sync/devices/$pythonDeviceId/sick_days.json'] =
|
|
pythonSickLog;
|
|
final sick = await SickDayPool.open(
|
|
persistence: MemPersistence(),
|
|
deviceId: 'phone',
|
|
);
|
|
final free = await FreeDayPool.open(
|
|
persistence: MemPersistence(),
|
|
deviceId: 'phone',
|
|
);
|
|
|
|
await runHeadlessPoolSync(
|
|
refresh: () => refreshDayPools(
|
|
openFree: () async => free,
|
|
openSick: () async => sick,
|
|
freeSync: FreeDaySync(
|
|
openClient: () async => remote,
|
|
scheduler: scheduler,
|
|
),
|
|
sickSync: SickDaySync(
|
|
openClient: () async => remote,
|
|
scheduler: scheduler,
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(caches['setSickDays'], ['2026-10-20']);
|
|
expect(caches['setFreeDays'], isEmpty);
|
|
expect(done, ['done']);
|
|
});
|
|
|
|
test('a pool that cannot open leaves the other one syncing', () async {
|
|
final remote = MemRemote();
|
|
remote.files['sickdays-sync/devices/pc/sick_days.json'] = pythonSickLog;
|
|
await refreshDayPools(
|
|
openFree: () async => throw const FormatException('corrupt'),
|
|
openSick: () =>
|
|
SickDayPool.open(persistence: MemPersistence(), deviceId: 'phone'),
|
|
sickSync: SickDaySync(
|
|
openClient: () async => remote,
|
|
scheduler: scheduler,
|
|
),
|
|
);
|
|
expect(caches['setSickDays'], ['2026-10-20']);
|
|
expect(caches.containsKey('setFreeDays'), isFalse);
|
|
});
|
|
|
|
test('done is sent even when the refresh fails', () async {
|
|
await expectLater(
|
|
runHeadlessPoolSync(refresh: () async => throw StateError('boom')),
|
|
throwsStateError,
|
|
);
|
|
expect(done, ['done']);
|
|
});
|
|
|
|
test('a missing done channel is swallowed', () async {
|
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
|
.setMockMethodCallHandler(poolChannel, (call) async {
|
|
throw PlatformException(code: 'gone');
|
|
});
|
|
await runHeadlessPoolSync(refresh: () async {});
|
|
});
|
|
}
|