feat(alarm): a refresh cadence for the upcoming list, still read-only

`upcoming` takes the cadence it re-resolves on, so a live countdown comes from
the engine's own resolution — the grace window, the watermarks and the DST
walk stay in one place instead of being copied into a screen. Re-resolving
writes nothing, which is asserted rather than assumed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wmy1BpCKi8KeSjaWhYuCPV
This commit is contained in:
makiolajandClaude Opus 5 committed 2026-09-12 14:43:33 +02:00
1 parent 431c4c2697
commit b5ec07b470
3 files changed
+96 -8

No files matched your search

@@ -39,6 +39,7 @@ import de.jeanlucmakiola.floret.prefs.PrefStore
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
@@ -892,6 +893,74 @@ class AlarmEngineTest {
assertThat(states.state(1L).handledOccurrence).isEqualTo(sevenToday)
assertThat(ring.ringing.value).isEqualTo(1L)
}
// --- M5: upcoming(refresh) (D8) ---
@Test
fun `upcoming with its default refresh still re-emits on a repository change`(@TempDir dir: Path) =
runTest {
givenAlarm(id = 1L, hour = 7)
val engine = engine(dir)
engine.upcoming().test {
assertThat(awaitItem().map { it.nextFire }).containsExactly(sevenToday)
alarms.create(AlarmDraft(time = TimeOfDay(6, 0)))
assertThat(awaitItem().map { it.nextFire })
.containsExactly(offsetInstant("2026-06-10T06:00+02:00"), sevenToday)
.inOrder()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `a refresh re-resolves against the new now, with no repository change`(@TempDir dir: Path) =
runTest {
givenAlarm(id = 1L, hour = 7, repeatDays = RepeatDays.EVERY_DAY)
val snoozeAt = wallClock.instant + 5.minutes
states.save(ringState(snoozedUntil = snoozeAt, snoozeCount = 1))
val engine = engine(dir)
val refresh = MutableSharedFlow<Unit>(replay = 1)
refresh.emit(Unit)
engine.upcoming(refresh).test {
val snoozed = awaitItem().single()
assertThat(snoozed.source to snoozed.nextFire).isEqualTo(FireSource.SNOOZE to snoozeAt)
// The snooze lapses with the passage of time alone: nothing was written.
wallClock.advance(20.minutes)
refresh.emit(Unit)
val lapsed = awaitItem().single()
assertThat(lapsed.source to lapsed.nextFire)
.isEqualTo(FireSource.OCCURRENCE to sevenToday)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `collecting upcoming across several refreshes writes nothing`(@TempDir dir: Path) = runTest {
givenAlarm(repeatDays = RepeatDays.EVERY_DAY, skipNextOccurrence = true)
val engine = engine(dir)
val before = stateDao.stored
val writesBefore = stateDao.writes
val refresh = MutableSharedFlow<Unit>(replay = 1)
refresh.emit(Unit)
engine.upcoming(refresh).test {
awaitItem()
repeat(3) {
wallClock.advance(1.minutes)
refresh.emit(Unit)
awaitItem()
}
cancelAndIgnoreRemainingEvents()
}
assertThat(stateDao.writes).isEqualTo(writesBefore)
assertThat(stateDao.stored).isEqualTo(before)
}
}
/**