fix(contacts): scope managed-event queries + fix reminder fire hour
Two provider-level bugs in the managed-event data path: - queryManagedEvents and applyManagedCalendarReminders matched every event in the calendar (UID_2445 IS NOT NULL / no filter). A stray user event there (e.g. an .ics import) was treated as 'existing but not desired' and deleted, or had its own reminders wiped and all-day-re-encoded. Both now match only our own mirror events (the 'contact-' UID prefix). - All-day reminder offsets were sampled at the event's DTSTART, which for a year-less birthday is the 1972 leap anchor — a year whose timezone offset (pre-DST) differs from today's, skewing every modern occurrence by up to an hour. The offset is now sampled at the upcoming occurrence (nextYearlyOccurrence), leaving only the inherent ±1h DST drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,43 @@ class AllDayReminderEncodingTest {
|
||||
assertThat(toProviderAllDayMinutes(-1, summer, berlin, nineAm)).isEqualTo(-1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next yearly occurrence wraps to next year once this year's date has passed`() {
|
||||
val today = LocalDate.of(2026, 8, 1)
|
||||
assertThat(nextYearlyOccurrence(5, 14, today)).isEqualTo(LocalDate.of(2027, 5, 14))
|
||||
assertThat(nextYearlyOccurrence(12, 25, today)).isEqualTo(LocalDate.of(2026, 12, 25))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next yearly occurrence returns today when the date is today`() {
|
||||
val today = LocalDate.of(2026, 3, 10)
|
||||
assertThat(nextYearlyOccurrence(3, 10, today)).isEqualTo(today)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `next yearly occurrence skips non-leap years for a Feb-29 date`() {
|
||||
assertThat(nextYearlyOccurrence(2, 29, LocalDate.of(2026, 6, 1)))
|
||||
.isEqualTo(LocalDate.of(2028, 2, 29))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sampling the offset at the next occurrence, not a 1972 anchor, fixes the fire hour`() {
|
||||
// A year-less birthday's DTSTART anchor is 1972 (Berlin had no DST then,
|
||||
// UTC+1). Sampling the offset there skews a modern CEST occurrence by an
|
||||
// hour; sampling at the upcoming occurrence fires at the intended time.
|
||||
val anchor1972 = LocalDate.of(1972, 7, 15)
|
||||
val nextOccurrence = nextYearlyOccurrence(7, 15, LocalDate.of(2026, 6, 1)) // 2026-07-15, CEST
|
||||
val rawFromAnchor = toProviderAllDayMinutes(0, anchor1972, berlin, nineAm)
|
||||
val rawFromNext = toProviderAllDayMinutes(0, nextOccurrence, berlin, nineAm)
|
||||
|
||||
val fireFromAnchor = actualFire(rawFromAnchor, nextOccurrence)
|
||||
.let(java.time.Instant::ofEpochMilli).atZone(berlin).toLocalTime()
|
||||
val fireFromNext = actualFire(rawFromNext, nextOccurrence)
|
||||
.let(java.time.Instant::ofEpochMilli).atZone(berlin).toLocalTime()
|
||||
assertThat(fireFromAnchor).isEqualTo(LocalTime.of(10, 0)) // skewed +1h
|
||||
assertThat(fireFromNext).isEqualTo(LocalTime.of(9, 0)) // correct
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a winter-anchored offset drifts one hour on a summer occurrence`() {
|
||||
// Known limitation: one fixed MINUTES per series can't track DST. An
|
||||
|
||||
Reference in New Issue
Block a user