Showing
3 changed files
with
80 additions
and
14 deletions
| @@ -64,7 +64,7 @@ class HealthRawLocalNotificationBuilder { | @@ -64,7 +64,7 @@ class HealthRawLocalNotificationBuilder { | ||
| 64 | realtimeSleepIntervals.isEmpty && latestRealtimeStressPoint != null | 64 | realtimeSleepIntervals.isEmpty && latestRealtimeStressPoint != null |
| 65 | ? [_defaultSleepInterval(latestRealtimeStressPoint.rawEndTime)] | 65 | ? [_defaultSleepInterval(latestRealtimeStressPoint.rawEndTime)] |
| 66 | : realtimeSleepIntervals; | 66 | : realtimeSleepIntervals; |
| 67 | - return [ | 67 | + return _latestNotificationsByRecordType([ |
| 68 | if (_sleepNotification(result.sleepResults, hasExistingSleep) | 68 | if (_sleepNotification(result.sleepResults, hasExistingSleep) |
| 69 | case final notification?) | 69 | case final notification?) |
| 70 | notification, | 70 | notification, |
| @@ -79,7 +79,22 @@ class HealthRawLocalNotificationBuilder { | @@ -79,7 +79,22 @@ class HealthRawLocalNotificationBuilder { | ||
| 79 | ) | 79 | ) |
| 80 | case final notification?) | 80 | case final notification?) |
| 81 | notification, | 81 | notification, |
| 82 | - ]; | 82 | + ]); |
| 83 | + } | ||
| 84 | + | ||
| 85 | + List<HealthRawLocalNotification> _latestNotificationsByRecordType( | ||
| 86 | + List<HealthRawLocalNotification> notifications, | ||
| 87 | + ) { | ||
| 88 | + if (notifications.length < 2) return notifications; | ||
| 89 | + final latestByType = | ||
| 90 | + <HealthRawLocalNotificationRecordType, HealthRawLocalNotification>{}; | ||
| 91 | + for (final notification in notifications) { | ||
| 92 | + final existing = latestByType[notification.recordType]; | ||
| 93 | + if (existing == null || notification.recordTime > existing.recordTime) { | ||
| 94 | + latestByType[notification.recordType] = notification; | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | + return latestByType.values.toList(growable: false); | ||
| 83 | } | 98 | } |
| 84 | 99 | ||
| 85 | HealthRawLocalNotification? _sleepNotification( | 100 | HealthRawLocalNotification? _sleepNotification( |
| @@ -694,7 +694,7 @@ void main() { | @@ -694,7 +694,7 @@ void main() { | ||
| 694 | }); | 694 | }); |
| 695 | 695 | ||
| 696 | test( | 696 | test( |
| 697 | - 'startCoreCaculate sends hrv and sleep notifications for new non-first results', | 697 | + 'startCoreCaculate sends only latest hrv notification for new non-first results', |
| 698 | () async { | 698 | () async { |
| 699 | final now = DateTime.now(); | 699 | final now = DateTime.now(); |
| 700 | final day = DateTime(now.year, now.month, now.day); | 700 | final day = DateTime(now.year, now.month, now.day); |
| @@ -770,11 +770,11 @@ void main() { | @@ -770,11 +770,11 @@ void main() { | ||
| 770 | readChunkDays: 1, | 770 | readChunkDays: 1, |
| 771 | ); | 771 | ); |
| 772 | 772 | ||
| 773 | - expect( | ||
| 774 | - notificationDispatcher.sentNotifications.where( | ||
| 775 | - (e) => e.recordType == HealthRawLocalNotificationRecordType.hrv), | ||
| 776 | - hasLength(3), | ||
| 777 | - ); | 773 | + final hrvNotifications = notificationDispatcher.sentNotifications |
| 774 | + .where((e) => e.recordType == HealthRawLocalNotificationRecordType.hrv) | ||
| 775 | + .toList(); | ||
| 776 | + expect(hrvNotifications, hasLength(1)); | ||
| 777 | + expect(hrvNotifications.single.recordTime, base + 720); | ||
| 778 | expect( | 778 | expect( |
| 779 | notificationDispatcher.sentNotifications.where( | 779 | notificationDispatcher.sentNotifications.where( |
| 780 | (e) => e.recordType == HealthRawLocalNotificationRecordType.sleep, | 780 | (e) => e.recordType == HealthRawLocalNotificationRecordType.sleep, |
| @@ -792,12 +792,17 @@ void main() { | @@ -792,12 +792,17 @@ void main() { | ||
| 792 | ); | 792 | ); |
| 793 | expect( | 793 | expect( |
| 794 | rows.hrvRows | 794 | rows.hrvRows |
| 795 | - .where((row) => [base + 120, base + 420, base + 720] | ||
| 796 | - .contains(row['raw_end_time'])) | ||
| 797 | - .every((row) => row['push_send_time'] != null), | 795 | + .where( |
| 796 | + (row) => [base + 120, base + 420].contains(row['raw_end_time'])) | ||
| 797 | + .every((row) => row['push_send_time'] == null), | ||
| 798 | isTrue, | 798 | isTrue, |
| 799 | ); | 799 | ); |
| 800 | expect( | 800 | expect( |
| 801 | + rows.hrvRows.singleWhere( | ||
| 802 | + (row) => row['raw_end_time'] == base + 720)['push_send_time'], | ||
| 803 | + isNotNull, | ||
| 804 | + ); | ||
| 805 | + expect( | ||
| 801 | rows.sleepRows.singleWhere( | 806 | rows.sleepRows.singleWhere( |
| 802 | (row) => row['date'] == LocalHealthDataConvert.unixSeconds(sleepEnd), | 807 | (row) => row['date'] == LocalHealthDataConvert.unixSeconds(sleepEnd), |
| 803 | )['push_send_time'], | 808 | )['push_send_time'], |
| @@ -153,8 +153,7 @@ void main() { | @@ -153,8 +153,7 @@ void main() { | ||
| 153 | expect(notifications.single.recordTime, latestTime); | 153 | expect(notifications.single.recordTime, latestTime); |
| 154 | }); | 154 | }); |
| 155 | 155 | ||
| 156 | - test('builds hrv notifications for every candidate including sleep likely', | ||
| 157 | - () { | 156 | + test('builds only the latest hrv notification for multiple candidates', () { |
| 158 | final firstTime = _seconds(DateTime(2026, 1, 1, 9)); | 157 | final firstTime = _seconds(DateTime(2026, 1, 1, 9)); |
| 159 | final secondTime = _seconds(DateTime(2026, 1, 1, 10)); | 158 | final secondTime = _seconds(DateTime(2026, 1, 1, 10)); |
| 160 | final notifications = builder.build( | 159 | final notifications = builder.build( |
| @@ -173,8 +172,55 @@ void main() { | @@ -173,8 +172,55 @@ void main() { | ||
| 173 | record: const HealthRawLocalNotificationRecord(), | 172 | record: const HealthRawLocalNotificationRecord(), |
| 174 | ); | 173 | ); |
| 175 | 174 | ||
| 175 | + expect(notifications, hasLength(1)); | ||
| 176 | + expect(notifications.single.recordType, | ||
| 177 | + HealthRawLocalNotificationRecordType.hrv); | ||
| 178 | + expect(notifications.single.recordTime, secondTime); | ||
| 179 | + }); | ||
| 180 | + | ||
| 181 | + test('keeps the latest notification per type without dropping other types', | ||
| 182 | + () { | ||
| 183 | + final hrvFirstTime = _seconds(DateTime(2026, 1, 1, 9)); | ||
| 184 | + final hrvSecondTime = _seconds(DateTime(2026, 1, 1, 10)); | ||
| 185 | + final realtimeBase = _seconds(DateTime(2026, 1, 1, 11)); | ||
| 186 | + final notifications = builder.build( | ||
| 187 | + result: HealthRawStressCalculationResult( | ||
| 188 | + userId: 1, | ||
| 189 | + hrvStressPoints: [ | ||
| 190 | + _hrvPoint(hrvFirstTime), | ||
| 191 | + _hrvPoint(hrvSecondTime), | ||
| 192 | + ], | ||
| 193 | + realtimeStressPoints: [ | ||
| 194 | + _realtimePoint(realtimeBase + 9 * 300, 70), | ||
| 195 | + ], | ||
| 196 | + dailyStressPoints: const [], | ||
| 197 | + ), | ||
| 198 | + hasExistingHrv: true, | ||
| 199 | + hasExistingSleep: false, | ||
| 200 | + realtimeWindow: [ | ||
| 201 | + for (var i = 0; i < 10; i++) _realtimePoint(realtimeBase + i * 300, 70), | ||
| 202 | + ], | ||
| 203 | + record: const HealthRawLocalNotificationRecord(), | ||
| 204 | + ); | ||
| 205 | + | ||
| 176 | expect(notifications, hasLength(2)); | 206 | expect(notifications, hasLength(2)); |
| 177 | - expect(notifications.map((e) => e.recordTime), [firstTime, secondTime]); | 207 | + expect( |
| 208 | + notifications | ||
| 209 | + .where( | ||
| 210 | + (e) => e.recordType == HealthRawLocalNotificationRecordType.hrv) | ||
| 211 | + .single | ||
| 212 | + .recordTime, | ||
| 213 | + hrvSecondTime, | ||
| 214 | + ); | ||
| 215 | + expect( | ||
| 216 | + notifications | ||
| 217 | + .where((e) => | ||
| 218 | + e.recordType == | ||
| 219 | + HealthRawLocalNotificationRecordType.realtimeStress) | ||
| 220 | + .single | ||
| 221 | + .recordTime, | ||
| 222 | + realtimeBase + 9 * 300, | ||
| 223 | + ); | ||
| 178 | }); | 224 | }); |
| 179 | 225 | ||
| 180 | test('builds realtime stress notification from latest 60 minute window', () { | 226 | test('builds realtime stress notification from latest 60 minute window', () { |
-
Please register or login to post a comment