fix(alarms): a consumed skip stays handled, and only a swipe hides a notice

A skip spent on its occurrence now becomes the handled-occurrence watermark, so a second resolution inside the fire grace cannot ring the skipped alarm after all. The upcoming notice records a swipe through its delete intent rather than inferring one, so a reboot or a blocked channel no longer hides it for good; its Dismiss asks for an unlock, and a Dismiss for a switched-off alarm clears its notice.

Fixes #16
This commit is contained in:
makiolaj committed 2026-10-21 20:18:05 +02:00
1 parent 5b35ef65d4
commit dedcbd5dfd
9 files changed
+122 -42

No files matched your search

@@ -17,6 +17,7 @@ class AlarmIntentsTest {
AlarmIntents.ACTION_DISMISS,
AlarmIntents.ACTION_UPCOMING_CHECK,
AlarmIntents.ACTION_DISMISS_UPCOMING,
AlarmIntents.ACTION_UPCOMING_SWIPED,
)
@Test
@@ -33,6 +34,7 @@ class AlarmIntentsTest {
AlarmIntents.REQUEST_UPCOMING_CHECK,
AlarmIntents.REQUEST_DISMISS_UPCOMING,
AlarmIntents.REQUEST_SHOW_UPCOMING,
AlarmIntents.REQUEST_UPCOMING_SWIPED,
)
}
@@ -257,6 +257,23 @@ class AlarmResolverTest {
assertThat(resolution.nextFire).isEqualTo(offsetInstant("2026-06-11T07:00+02:00"))
}
@Test
fun `a consumed skip cannot ring on the next pass through the fire grace`() {
val alarm = alarmAt(hour = 7, repeatDays = RepeatDays.EVERY_DAY, skipNextOccurrence = true)
val watermark = offsetInstant("2026-06-10T07:00+02:00")
val first = resolve(alarm, ringState(skippedOccurrence = watermark), now = watermark + 30.seconds)
// The engine persists the first pass: the flag cleared, the state saved.
val second = resolve(
alarm.copy(skipNextOccurrence = false),
first.state,
now = watermark + 40.seconds,
)
assertThat(first.state.handledOccurrence).isEqualTo(watermark)
assertThat(second.nextFire).isEqualTo(offsetInstant("2026-06-11T07:00+02:00"))
}
@Test
fun `skipping a one-shot alarm disables it`() {
val alarm = alarmAt(hour = 7, skipNextOccurrence = true)