Fix cached reminder recovery and retain pre-reconcile idle diagnostics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -37,6 +37,14 @@ class _Notifications extends FakeNotifications {
|
||||
'model': 'TestModel',
|
||||
'sdk': 35,
|
||||
'backgroundRestricted': true,
|
||||
'standbyBucket': 45,
|
||||
'startupAlarmAudit': {
|
||||
'capturedAtEpochMillis': 123,
|
||||
'cachedCourseCount': 3,
|
||||
'existingHandleCount': 1,
|
||||
'missingHandleCount': 2,
|
||||
'courseName': 'Private course name',
|
||||
},
|
||||
'account': 'private@example.com',
|
||||
},
|
||||
'courseNames': ['Private course name'],
|
||||
@@ -129,7 +137,7 @@ void main() {
|
||||
final previous = notifications.reconciliations;
|
||||
await tester.tap(find.text('检查提醒'));
|
||||
await tester.pump();
|
||||
expect(notifications.reconciliationsAtDiagnosis, previous + 1);
|
||||
expect(notifications.reconciliationsAtDiagnosis, previous);
|
||||
for (final button in tester.widgetList<TextButton>(
|
||||
find.byType(TextButton),
|
||||
)) {
|
||||
@@ -174,5 +182,12 @@ void main() {
|
||||
expect(report['courseCount'], 0);
|
||||
expect(report['appVersion'], isNotEmpty);
|
||||
expect((report['system'] as Map)['model'], 'TestModel');
|
||||
expect(
|
||||
((report['system'] as Map)['startupAlarmAudit']
|
||||
as Map)['missingHandleCount'],
|
||||
2,
|
||||
);
|
||||
expect((report['beforeCheck'] as Map)['pendingCount'], 2);
|
||||
expect(find.textContaining('系统待机限制'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user