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
147 lines
4.8 KiB
Dart
147 lines
4.8 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
|
|
|
import '../fake_day_pool_store.dart';
|
|
|
|
final _today = DateTime(2026, 10, 16);
|
|
bool _never(DateTime _) => false;
|
|
|
|
void main() {
|
|
late MemPersistence disk;
|
|
late SickDayPool pool;
|
|
|
|
setUp(() async {
|
|
disk = MemPersistence();
|
|
pool = await SickDayPool.open(persistence: disk, deviceId: 'phone-uuid');
|
|
});
|
|
|
|
test('every day is normal until marked', () {
|
|
expect(pool.isSick(_today), isFalse);
|
|
expect(pool.sickDays(), isEmpty);
|
|
});
|
|
|
|
test('marks today and a future day, ascending, in the wire format', () async {
|
|
await pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today);
|
|
await pool.add(_today, isFree: _never, now: _today);
|
|
|
|
expect(pool.isSick(DateTime(2026, 10, 20, 14)), isTrue);
|
|
expect(pool.sickIsoDates(), ['2026-10-16', '2026-10-20']);
|
|
final json = jsonDecode(disk.text!) as Map<String, Object?>;
|
|
final record = json['2026-10-20']! as Map<String, Object?>;
|
|
final fields = record['fields']! as Map<String, Object?>;
|
|
final state = fields['state']! as List<Object?>;
|
|
final actor = fields['actor']! as List<Object?>;
|
|
expect(state.first, 'sick');
|
|
expect(actor.first, 'phone-uuid');
|
|
expect(state.last, actor.last, reason: 'one HLC tick per write');
|
|
expect(record['deleted'], isFalse);
|
|
});
|
|
|
|
test('clearing flips state to cleared and never tombstones', () async {
|
|
await pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today);
|
|
await pool.remove(DateTime(2026, 10, 20), now: _today);
|
|
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isFalse);
|
|
final record = pool.snapshot()['2026-10-20']!;
|
|
expect(record.deleted, isFalse);
|
|
expect(record.fields['state']!.$1, 'cleared');
|
|
|
|
// ...so it can be marked again.
|
|
await pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today);
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isTrue);
|
|
});
|
|
|
|
test('refuses a past day for both add and remove', () async {
|
|
final yesterday = DateTime(2026, 10, 15);
|
|
await expectLater(
|
|
pool.add(yesterday, isFree: _never, now: _today),
|
|
throwsA(
|
|
isA<SickDayPolicyError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
contains('2026-10-15 has already passed'),
|
|
),
|
|
),
|
|
);
|
|
await expectLater(
|
|
pool.remove(yesterday, now: _today),
|
|
throwsA(isA<SickDayPolicyError>()),
|
|
);
|
|
});
|
|
|
|
test('refuses an already-sick day and clearing a normal day', () async {
|
|
await pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today);
|
|
await expectLater(
|
|
pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today),
|
|
throwsA(
|
|
isA<SickDayPolicyError>().having(
|
|
(e) => e.toString(),
|
|
'toString',
|
|
'2026-10-20 is already a sick day.',
|
|
),
|
|
),
|
|
);
|
|
await expectLater(
|
|
pool.remove(DateTime(2026, 10, 22), now: _today),
|
|
throwsA(
|
|
isA<SickDayPolicyError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
'2026-10-22 is not a sick day.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('refuses a free day and names the free-day pool', () async {
|
|
await expectLater(
|
|
pool.add(
|
|
DateTime(2026, 10, 20),
|
|
isFree: (day) => day == DateTime(2026, 10, 20),
|
|
now: _today,
|
|
),
|
|
throwsA(
|
|
isA<SickDayPolicyError>().having(
|
|
(e) => e.message,
|
|
'message',
|
|
'2026-10-20 is a free day — release it in Free days first.',
|
|
),
|
|
),
|
|
);
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isFalse);
|
|
});
|
|
|
|
test('reads a log the Python package wrote', () async {
|
|
await pool.replaceAll(logFromJson(pythonSickLog));
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isTrue);
|
|
expect(pool.isSick(DateTime(2026, 10, 21)), isFalse, reason: 'cleared');
|
|
expect(pool.sickIsoDates(), ['2026-10-20']);
|
|
});
|
|
|
|
test('a tombstone, an unknown state or a junk id is not sick', () async {
|
|
final hlc = Hlc.newTick('pc');
|
|
await pool.replaceAll({
|
|
'2026-10-20': Record(
|
|
id: '2026-10-20',
|
|
fields: {'state': ('sick', hlc)},
|
|
deleted: true,
|
|
deletedHlc: hlc,
|
|
),
|
|
'2026-10-22': Record(id: '2026-10-22', fields: {'state': ('ill', hlc)}),
|
|
'nonsense': Record(id: 'nonsense', fields: {'state': ('sick', hlc)}),
|
|
});
|
|
expect(pool.isSick(DateTime(2026, 10, 20)), isFalse);
|
|
expect(pool.isSick(DateTime(2026, 10, 22)), isFalse);
|
|
expect(pool.sickIsoDates(), isEmpty);
|
|
});
|
|
|
|
test('persists across a reopen', () async {
|
|
await pool.add(DateTime(2026, 10, 20), isFree: _never, now: _today);
|
|
final again = await SickDayPool.open(persistence: disk, deviceId: 'x');
|
|
expect(again.isSick(DateTime(2026, 10, 20)), isTrue);
|
|
});
|
|
}
|