Skip to content

test(notify): anchor dedupe keys to today — CI is red on every PR because a date aged out - #207

Merged
abdulsaheel merged 1 commit into
mainfrom
fix/retention-window-timebomb-tests
Aug 7, 2026
Merged

test(notify): anchor dedupe keys to today — CI is red on every PR because a date aged out#207
abdulsaheel merged 1 commit into
mainfrom
fix/retention-window-timebomb-tests

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

This is currently blocking CI on every open PR. Nothing is wrong with any of them, and nothing is wrong with lib/ — the calendar advanced.

What happened

notification_dedupe_test builds date-prefixed dedupe keys from a hardcoded 2026-07-23. 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 instant 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 that date was 12 days old. It has been failing ever since the window closed — on the same commit, with no code change and nothing to point at.

Proof before fix

Substituting today's date into the unmodified file on origin/main turns all 15 tests green. That isolates the cause completely: the guard was never broken.

The fix

Anchor to todayLabel() (already imported in this file) plus a small _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. Full suite on this branch: 1201 passing, 0 failing.

Why it matters beyond these 6 tests

flutter test --concurrency=1 is exactly what test.yml runs. From the moment that window closed, every open PR's CI went red regardless of content. Merging this unblocks all of them.

Follow-up worth considering

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 aren't retention-sensitive, but any test whose subject prunes or windows on age carries the same failure mode latent. A lint or a shared todayLabel()-based fixture helper would close the class.

Also note: fork PRs are currently sitting at action_required — they need "Approve and run workflows" before any of them can show a green check.

Summary by CodeRabbit

  • Tests
    • Updated notification deduplication tests to use dynamically generated dates.
    • Preserved coverage for persistence, permissions, category gating, quiet hours, concurrency, retention, and high-volume alerts.
    • Improved test reliability across different calendar dates.

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.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The notification dedupe tests now generate date labels and dedupe keys from today and tomorrow. Existing coverage remains for persistence, permissions, categories, quiet hours, concurrency, retention, and high-stress alerts.

Changes

Notification dedupe test updates

Layer / File(s) Summary
Dynamic date generation and dedupe coverage
test/notification_dedupe_test.dart
Test helpers generate today- and tomorrow-based dates. All deduplication scenarios use dynamic keys and dates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • OpenStrap/edge#137: Introduced the notification deduplication test suite updated by this change.

Suggested reviewers: dannymcc

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the test fix and explains that outdated hardcoded dates caused recurring CI failures.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/notification_dedupe_test.dart`:
- Around line 80-87: Update the test label initialization around _today,
_tomorrow, and _dayLabelOffset to capture a single DateTime value, derive both
offsets from that timestamp, and reuse the canonical todayLabel formatter from
lib/data/day_label.dart. Remove the manual formatting in _dayLabelOffset while
preserving the expected today and tomorrow labels.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 28b68b73-480d-4557-bccc-5a4655fd164b

📥 Commits

Reviewing files that changed from the base of the PR and between d911f60 and c88f589.

📒 Files selected for processing (1)
  • test/notification_dedupe_test.dart

Comment on lines +80 to +87
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')}';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Derive both labels from one timestamp and the canonical formatter.

_dayLabelOffset calls DateTime.now() independently from _today. If initialization crosses local midnight, _tomorrow can skip a calendar day. The manual formatter can also diverge from todayLabel in lib/data/day_label.dart at Line [24].

Capture one DateTime and derive both labels with todayLabel.

Proposed fix
-final String _today = todayLabel();
-final String _tomorrow = _dayLabelOffset(1);
+final _testNow = DateTime.now();
+final String _today = todayLabel(_testNow);
+final String _tomorrow = _dayLabelOffset(_testNow, 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')}';
-}
+String _dayLabelOffset(DateTime base, int days) =>
+    todayLabel(DateTime(base.year, base.month, base.day + days));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/notification_dedupe_test.dart` around lines 80 - 87, Update the test
label initialization around _today, _tomorrow, and _dayLabelOffset to capture a
single DateTime value, derive both offsets from that timestamp, and reuse the
canonical todayLabel formatter from lib/data/day_label.dart. Remove the manual
formatting in _dayLabelOffset while preserving the expected today and tomorrow
labels.

@abdulsaheel
abdulsaheel merged commit a239141 into main Aug 7, 2026
1 check passed
abdulsaheel added a commit that referenced this pull request Aug 8, 2026
Four of the five findings were real. Each verified against the code before
changing anything.

1. VACUOUS GUARD TEST (Major) -- substrate_accel_absence_test only exercised
   `accelPresentFraction`, never the gate it feeds at substrate.dart:574-600.
   The coverage floor could have been deleted and every assertion still passed
   -- the same vacuity I flagged in other people's tests earlier in this stack,
   in my own. Now drives `calendarDays` (the real entry point) and asserts on
   `PhysioDay.sleepSource`.

   Two things worth recording about building it. The first positive control
   FAILED: a "near-still" night was not detected as sleep at all, which would
   have made the negative test pass for the wrong reason. Fixed by giving the
   fixture a genuine shape -- constant gravity through the night, a 10 deg/s
   ramp while awake (a ramp, not an alternation: the mask smooths z-angle with
   a 5-second rolling MEDIAN, which erases a 1 Hz square wave entirely).

   Second, mutation testing showed the headline all-zero case was STILL not
   discriminating -- with the gate removed it passed anyway, because a record
   immobile end to end is rejected downstream regardless. The discriminating
   shape is the realistic one: evening has gravity, the night's records do not.
   Coverage lands below the floor, and those zeros would otherwise form a clean
   multi-hour "immobile" block to anchor on. Removing the gate now fails 2
   tests; both are pinned, with the non-discriminating one kept and labelled.

2. STALE SERIAL (Minor, real) -- `context.select` selected `a.device`, but
   `select` compares with `==`, `DeviceState` declares no `==`/`hashCode`, and
   `BleEngine` mutates `state.serial` IN PLACE. The selector returned the same
   reference before and after, so no rebuild fired and the row could sit on a
   stale serial indefinitely. Now selects the serial STRING the row renders,
   which is the only value this sheet reads from `device`/`paired`.

3. CHIP OVERFLOW (Minor) -- two intrinsically-sized, non-flex chips in a Row,
   the second carrying "WHOOP 5 (experimental)". At large text scales or on a
   narrow device their combined width exceeds the Expanded column. Wrap
   degrades to a second line instead.

4. TEST COVERAGE (Minor) -- the R10-lite case asserted only ABSENCE from
   `decoded_onehz`, which would equally pass if the record were dropped
   outright; retention in `samples` is the other half of that contract. Added,
   plus a case protecting the unparseable-hex fallback in
   `_decodeOneHzSample`.

NOT CHANGED, deliberately:

  * `gen5DeepBuffersEnabled` is unwired in production -- verified true (zero
    overrides in app_state or background_sync), but it is a deliberate
    default-OFF opt-in for v20/v21/v26 buffers that are archived, not
    interpreted. Wiring a settings toggle for a feature nothing consumes would
    be premature.

  * The protocol PIN. CodeRabbit's `pubspec.yaml` comment finding was real and
    is fixed -- the file claimed this branch was "intentionally WHOOP-4-only"
    while pinning the multiband commit twelve lines below. But the pin itself
    is left alone: protocol#16 has landed, so the SHA is now reachable from
    protocol main and SHOULD be repointed there (the current pin is a
    PR-branch head on the branch this PR deletes on merge -- the exact
    orphaning the same paragraph warns against). That belongs with the
    main-merge, not this pass; recorded as a FOLLOW-UP in the file.

12 tests added; the gate tests are mutation-verified. analyze clean.
Suite 1085 passing / 6 failing -- the 6 are the notification_dedupe time bombs
this branch still carries because it predates #207 on main (19 hardcoded dates,
no `todayLabel()`). They clear when main is merged in.
@abdulsaheel
abdulsaheel deleted the fix/retention-window-timebomb-tests branch August 24, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant