From c88f5891d6b517af9e6df89876a7e9d5341ab51c Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Fri, 7 Aug 2026 01:19:03 +0530 Subject: [PATCH] test(notify): anchor dedupe keys to today, not a date that ages out The 6 failures in notification_dedupe_test are a TIME BOMB, not a regression. Nothing in the code changed: the calendar did. The suite builds date-PREFIXED dedupe keys from a hardcoded `2026-07-23`, and `FiredKeyStore` prunes dated flags older than `retentionDays` (14). While that date was recent the keys stayed inside the window and the guard deduped correctly. Once it aged past 14 days, every key was pruned the moment it was written, so the second and third emit fired again: same dedupeKey fires the OS notification exactly once Expected: <1> Actual: <3> main passed CI on 2026-08-04, when the date was 12 days old. It has been failing since the window closed, on the same commit, with no code change and nothing to point at. Proved it directly before fixing: substituting today's date into the unmodified file on origin/main turns all 15 tests green. Fix is to anchor to `todayLabel()` (already imported here) plus a `_dayLabelOffset` helper for the next-day case, so the keys sit inside the retention window permanently -- which is the condition the dedupe guard is actually specified against. No lib/ change; the guard itself was always correct. Full suite on this branch: 1201 passing, 0 failing. WHY THIS MATTERS BEYOND THE 6 TESTS: `flutter test --concurrency=1` is exactly what test.yml runs, so from the moment that window closed EVERY open PR's CI went red regardless of content. This unblocks them all. Worth a follow-up: other suites hardcode dates too (ai_briefing_test, day_nav_test, readiness_freeze_test, core_screens_test, sleep_profile_policy_test, db_storage_hygiene_test). Most are not retention-sensitive, but any test whose subject prunes or windows on age has the same failure mode latent in it. --- test/notification_dedupe_test.dart | 65 ++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/test/notification_dedupe_test.dart b/test/notification_dedupe_test.dart index 8144058..a679a61 100644 --- a/test/notification_dedupe_test.dart +++ b/test/notification_dedupe_test.dart @@ -63,11 +63,34 @@ class _GatedSink { void release() => _gate.complete(); } +/// TODAY's label, never a hardcoded date. +/// +/// These keys are date-PREFIXED, and `FiredKeyStore` prunes dated flags older +/// than [FiredKeyStore.retentionDays] (14). A literal date is therefore a time +/// bomb: it works while it is recent, then on one particular morning ages out +/// of the retention window and every dedupe assertion in this file starts +/// failing at once -- with no code change and nothing to point at. +/// +/// That is exactly what happened. This suite was written around a fixed date in +/// July, passed CI on 2026-08-04 while it was 12 days old, and began failing +/// once it fell outside the window. Anchoring to `todayLabel()` keeps the keys +/// inside the retention window permanently, which is the condition the dedupe +/// guard is actually specified against. +final String _today = todayLabel(); +final String _tomorrow = _dayLabelOffset(1); + +String _dayLabelOffset(int days) { + final d = DateTime.now().add(Duration(days: days)); + return '${d.year.toString().padLeft(4, '0')}-' + '${d.month.toString().padLeft(2, '0')}-' + '${d.day.toString().padLeft(2, '0')}'; +} + NotificationEvent _ev( String dedupeKey, { NotifCategory category = NotifCategory.health, NotifPriority priority = NotifPriority.critical, - String date = '2026-07-23', + String? date, }) => NotificationEvent( dedupeKey: dedupeKey, @@ -75,7 +98,7 @@ NotificationEvent _ev( priority: priority, title: 't', body: 'b', - date: date, + date: date ?? _today, ); void main() { @@ -101,7 +124,7 @@ void main() { final sink = _FakeSink(); center.presentSink = sink.call; - final e = _ev('2026-07-23:irregular'); + final e = _ev('$_today:irregular'); await center.emit(e); await center.emit(e); // re-derive would re-emit the same key await center.emit(e); @@ -113,13 +136,13 @@ void main() { final sink = _FakeSink(); center.presentSink = sink.call; - await center.emit(_ev('2026-07-23:irregular', date: '2026-07-23')); - await center.emit(_ev('2026-07-24:irregular', date: '2026-07-24')); + await center.emit(_ev('$_today:irregular', date: _today)); + await center.emit(_ev('$_tomorrow:irregular', date: _tomorrow)); expect(sink.shown.length, 2); expect( sink.shown.map((e) => e.dedupeKey), - containsAll(['2026-07-23:irregular', '2026-07-24:irregular']), + containsAll(['$_today:irregular', '$_tomorrow:irregular']), ); }); @@ -128,14 +151,14 @@ void main() { // the same on-disk store that survives an app restart on-device. final sink1 = _FakeSink(); center.presentSink = sink1.call; - await center.emit(_ev('2026-07-23:illness')); + await center.emit(_ev('$_today:illness')); expect(sink1.shown.length, 1); // Second "session": same persisted store — the key is still remembered, // so it must NOT fire again. final sink2 = _FakeSink(); center.presentSink = sink2.call; - await center.emit(_ev('2026-07-23:illness')); + await center.emit(_ev('$_today:illness')); expect(sink2.shown, isEmpty); }); @@ -144,11 +167,11 @@ void main() { // still lets it fire. final denied = _FakeSink(grant: false); center.presentSink = denied.call; - await center.emit(_ev('2026-07-23:temp')); + await center.emit(_ev('$_today:temp')); final granted = _FakeSink(); center.presentSink = granted.call; - await center.emit(_ev('2026-07-23:temp')); + await center.emit(_ev('$_today:temp')); expect(granted.shown.length, 1); }); }); @@ -159,12 +182,12 @@ void main() { final sink = _FakeSink(); center.presentSink = sink.call; - await center.emit(_ev('2026-07-23:illness', category: NotifCategory.health)); + await center.emit(_ev('$_today:illness', category: NotifCategory.health)); expect(sink.shown, isEmpty); // Re-enabling the category later must let the key fire — the gate, not the // dedupe guard, suppressed it, so no key should have been recorded. - expect(await const FiredKeyStore().hasFired('2026-07-23:illness'), isFalse); + expect(await const FiredKeyStore().hasFired('$_today:illness'), isFalse); }); test('quiet hours suppress a non-critical event', () async { @@ -178,7 +201,7 @@ void main() { center.presentSink = sink.call; await center.emit(_ev( - '2026-07-23:recovery', + '$_today:recovery', category: NotifCategory.recovery, priority: NotifPriority.normal, )); @@ -195,7 +218,7 @@ void main() { final sink = _FakeSink(); center.presentSink = sink.call; - final e = _ev('2026-07-23:illness', priority: NotifPriority.critical); + final e = _ev('$_today:illness', priority: NotifPriority.critical); await center.emit(e); await center.emit(e); expect(sink.shown.length, 1); @@ -208,7 +231,7 @@ void main() { final sink = _GatedSink(); center.presentSink = sink.call; - final e = _ev('2026-07-23:irregular'); + final e = _ev('$_today:irregular'); final f1 = center.emit(e); final f2 = center.emit(e); // Order on the sink's entry signal, not a timer: once the first emit is @@ -228,8 +251,8 @@ void main() { final sink = _GatedSink(); center.presentSink = sink.call; - final f1 = center.emit(_ev('2026-07-23:a')); - final f2 = center.emit(_ev('2026-07-23:b')); + final f1 = center.emit(_ev('$_today:a')); + final f2 = center.emit(_ev('$_today:b')); // First emit is parked inside present; the second is held on the lock, so // its record-key write can only run after the first's — no interleaving. await sink.entered; @@ -239,8 +262,8 @@ void main() { expect(sink.calls, 2); // Independent per-key flags: neither key clobbered the other. const store = FiredKeyStore(); - expect(await store.hasFired('2026-07-23:a'), isTrue); - expect(await store.hasFired('2026-07-23:b'), isTrue); + expect(await store.hasFired('$_today:a'), isTrue); + expect(await store.hasFired('$_today:b'), isTrue); }); }); @@ -249,11 +272,11 @@ void main() { // (normal) priority, no route. It used to call presentEvent directly, // bypassing both the gate and the dedupe guard — now it goes through emit. NotificationEvent highStress() => NotificationEvent( - dedupeKey: '2026-07-23:high_stress', + dedupeKey: '$_today:high_stress', category: NotifCategory.health, title: 'High Stress Detected', body: 'Your stress score is 82. Consider taking a moment to breathe.', - date: '2026-07-23', + date: _today, ); test('dedupes on repeat (was previously re-alerting per screen visit)',