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 {
+16 -1
View File
@@ -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);
});
}