Skip to content

diag: say when an alarm readback has no stored frame - #2545

Merged
ryanbr merged 2 commits into
mainfrom
alarm-readback-frame-line
Sep 28, 2026
Merged

ryanbr merged 2 commits into
mainfrom
alarm-readback-frame-line

Conversation

@ryanbr

@ryanbr ryanbr commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Found reviewing a 2026-09-28 strap log. Its alarm block showed Strap reports: 2045-06-10 14:03 and no Readback frame: line at all, and establishing why cost a trip through git history.

The frame line was emitted only when the frame existed. A readback carrying an epoch and no frame therefore printed nothing, which reads as "the frame was checked and was fine" rather than "there is nothing to check". Worse, the two causes are indistinguishable from outside the app:

The readback in that log was taken 2026-08-26, two days before the capture existed. That is knowable only from the repo, not from the export, which is the wrong way round for a diagnostic whose whole job is to separate a genuinely-stored stale alarm from a misdecode of a fixed response field.

What changes

The line now always appears. When there is no frame it names both possibilities instead of staying silent, the way the strap-clock verdict a few lines above it already says why it abstains rather than printing nothing:

Readback frame: not stored (this readback predates the frame capture, or the write failed), so a
genuinely-stored stale alarm cannot be told from a misdecode of a fixed response field here

Worth noting for whoever reads this next: the writer stores at least "empty" for any frame it decodes, so this branch is reachable in practice only for records banked before the capture existed. It will stop firing for a given install once its next WHOOP 4.0 readback lands. That is the intent, not a gap.

Parity

Both platforms carry it. The Apple twin also had a blank-handling divergence in the same expression: Kotlin used isNotBlank() while Swift used !raw.isEmpty, so a whitespace-only value printed a frame line carrying no bytes on Apple and counted as absent on Android. Swift trims first now, so blank means not stored on both.

Verification

  • ./gradlew :app:testFullDebugUnitTest --no-build-cache --rerun-tasks: 763 suites, 6497 tests, 0 failures, result XML checked fresh against the clock.
  • ./gradlew :app:lintVitalFullRelease -PstagingRelease: clean.
  • 4 new Robolectric tests on Android and 4 Swift twins, both exercising the real alarmLines rather than asserting on source text. The Apple half is where the behaviour changed twice (the new branch, and blank handling), so it needed its own coverage; alarmLines there is a static func over UserDefaults.standard, seedable the way StrapProvidesLineTests already covers sibling lines.
  • The Swift absent-frame assertion pins the Kotlin text verbatim, not a substring. The two strings are byte-identical today and these lines exist so an Android report and an Apple one compare directly, so asserting each side against only itself would let them drift apart silently.
  • Mutation-checked on Android: removing the branch turns exactly the two new-behaviour tests red (absent frame, blank frame) and leaves both controls green (frame printed verbatim, no readback emits no line).
  • python3 Tools/doc_comment_lint.py: clean.
  • The Apple half is verified by the macOS and iOS legs rather than locally: this host is Linux.

Two things re-review caught, recorded because each would otherwise have shipped. The mutation run had the Kotlin branch deleted on disk when I re-read the diff, so the working tree looked like the fix was missing; judging a tree mid-mutation is as unsafe as editing one. And the Apple half had no tests at all, on the side where the behaviour changed twice.

The readback frame line was emitted only when the frame existed, so a readback carrying an epoch and
no frame printed nothing at all, which reads as "the frame was checked and was fine". The two causes
are indistinguishable from outside: #1707 began banking the frame on 2026-08-28, so any readback taken
before that has an epoch and no frame, and a write that failed looks identical.

A 2045 readback in a 2026-09-28 capture cost a trip through git history to establish that the record
simply predated the capture. The line now always appears and names both possibilities, the way the
strap-clock verdict above it already says why it abstains rather than printing no line.

Since the writer stores at least "empty" for any frame it decodes, this branch is reachable in practice
only for records banked before that capture existed, which is the intent.

Also aligns the Apple twin's blank handling with Kotlin's isNotBlank. A whitespace-only value printed a
frame line carrying no bytes there; it counts as not stored on both platforms now.

Four Robolectric tests. Mutation-checked: removing the branch turns exactly the two new-behaviour tests
red and leaves both controls green.
The branch landed on both platforms but only Android got tests, and the Apple half is where the
behaviour actually changed twice: the new else branch, and blank handling moving from isEmpty to a
trimmed check so a whitespace-only value stops printing a frame line carrying no bytes.

`alarmLines` is a static func reading UserDefaults.standard, so it is seedable directly, the way
StrapProvidesLineTests and OrphanedSamplesLineTests already cover sibling diagnostic lines. Keys are
cleared in both setUp and tearDown so no case can inherit another's readback.

The absent-frame assertion pins the Kotlin text verbatim, not merely a substring. These lines exist so
an Android report and an Apple one compare directly, and the two strings are byte-identical today, so
asserting each side against only itself would let them drift apart silently.
@ryanbr
ryanbr merged commit f0c287a into main Sep 28, 2026
6 checks passed
@ryanbr
ryanbr deleted the alarm-readback-frame-line branch September 28, 2026 03:29
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