Fix cached reminder recovery and retain pre-reconcile idle diagnostics

This commit is contained in:
david1465833828
2026-10-28 17:46:33 +08:00
parent 93bcfd534c
commit dc71e411e9
13 changed files with 350 additions and 6 deletions
@@ -7,6 +7,39 @@ import 'package:hepan_everyday/services/local_notification_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'system handle audit only accepts IDs from the requested cache',
() async {
const system = MethodChannel('com.tpschedule.app/reminders');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(system, (call) async {
expect(call.method, 'findMissingAlarmHandles');
expect((call.arguments as List).toSet(), {11, 22});
return [22, 999];
});
addTearDown(() => messenger.setMockMethodCallHandler(system, null));
expect(await PluginReminderGateway().missingSystemIds({11, 22}), {22});
},
);
test(
'optional handle audit failure does not prevent reconciliation',
() async {
const system = MethodChannel('com.tpschedule.app/reminders');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
var calls = 0;
messenger.setMockMethodCallHandler(system, (_) async {
calls++;
throw PlatformException(code: 'reminder_system_unavailable');
});
addTearDown(() => messenger.setMockMethodCallHandler(system, null));
final gateway = PluginReminderGateway();
expect(await gateway.missingSystemIds({11}), isEmpty);
expect(await gateway.missingSystemIds({}), isEmpty);
expect(calls, 1);
},
);
test('fresh notification initialization creates the course category before checks', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
AndroidFlutterLocalNotificationsPlugin.registerWith();
@@ -13,6 +13,15 @@ class AlarmGateway extends ReminderGateway {
final alarms = <int, ReminderRequest>{};
int permissionRequests = 0;
bool dropWrites = false;
final missingHandles = <int>{};
Set<int> lastAuditedIds = {};
int scheduleCalls = 0;
@override
Future<Set<int>> missingSystemIds(Set<int> ids) async {
lastAuditedIds = {...ids};
return missingHandles.intersection(ids);
}
@override
Future<ReminderPermissions> permissions({required bool request}) async {
if (request) permissionRequests++;
@@ -23,7 +32,9 @@ class AlarmGateway extends ReminderGateway {
Future<Set<int>> pendingIds() async => alarms.keys.toSet();
@override
Future<void> schedule(ReminderRequest value) async {
scheduleCalls++;
if (!dropWrites) alarms[value.id] = value;
missingHandles.remove(value.id);
}
@override
@@ -48,6 +59,32 @@ void main() {
return AppSnapshot.fromJson(json).settings;
}
test(
'overdue cached reminders are audited before removal on warm resume',
() async {
final events = ScheduleEngine.occurrences([course()], term);
await service.reconcile(events, settings, DateTime.utc(2026, 9, 9));
final cached = gateway.alarms.keys.toSet();
expect(cached, isNotEmpty);
await service.reconcile(events, settings, DateTime.utc(2027, 1, 1));
expect(gateway.lastAuditedIds, cached);
expect(gateway.alarms, isEmpty);
},
);
test('cached fingerprint does not hide a missing system trigger', () async {
final events = ScheduleEngine.occurrences([course()], term);
final now = DateTime.utc(2026, 9, 9);
await service.reconcile(events, settings, now);
final previous = gateway.scheduleCalls;
gateway.missingHandles.addAll(gateway.alarms.keys);
await service.reconcile(events, settings, now);
expect(gateway.scheduleCalls, previous + 1);
expect(gateway.missingHandles, isEmpty);
await service.reconcile(events, settings, now);
expect(gateway.scheduleCalls, previous + 1);
});
test(
'does not report success when native scheduling did not persist',
() async {