store: return one calendar row per event

CalendarEvents range-scanned the key prefix and returned every historical
row, voided ones included. The facts table is append-only and the event
key is day plus summary, so moving a standup from 14:00 to 16:00 left two
rows under one key. The day plan prints a time per line, so it recited
both and told the owner he had two standups.

The query now drops voided rows, keeps the latest row within a source,
and prefers the best-evidenced source across them, so a notification
relay guessing at a meeting cannot displace the calendar read of it.
Found in review of #58.
This commit is contained in:
kami
2026-08-01 14:01:57 +04:00
parent 4f012e350c
commit 38b09ded95
2 changed files with 109 additions and 2 deletions
+60
View File
@@ -381,6 +381,66 @@ func TestCalendarEvents(t *testing.T) {
}
}
// A rescheduled meeting keeps its key and appends a row. The query must return
// the current value, not the history: reciting both told the owner he had two
// standups when one had been moved.
func TestCalendarEventsReturnsOneRowPerEvent(t *testing.T) {
store := newTestStore(t)
defer store.Close()
ctx := context.Background()
day := time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC)
const key = "calendar_event_20260803_Standup"
store.WriteFact(ctx, day.Add(14*time.Hour), KindEnv, key,
`"Standup @ 14:00-14:30"`, calendar.SourcePersonal, 1.0, sql.NullInt64{})
store.WriteFact(ctx, day.Add(16*time.Hour), KindEnv, key,
`"Standup @ 16:00-16:30"`, calendar.SourcePersonal, 1.0, sql.NullInt64{})
// The notification relay guessed at the same meeting. A calendar read is
// better evidence, so the hedged row must not displace it.
store.WriteFact(ctx, day.Add(17*time.Hour), KindEnv, key,
`"Standup @ 17:00-17:30"`, calendar.SourceAmbient, calendar.AmbientConfidence, sql.NullInt64{})
events, err := store.CalendarEvents(ctx, day, day.AddDate(0, 0, 1))
if err != nil {
t.Fatalf("CalendarEvents: %v", err)
}
if len(events) != 1 {
t.Fatalf("got %d rows, want the current one only: %+v", len(events), events)
}
if events[0].Value != `"Standup @ 16:00-16:30"` {
t.Errorf("value = %q, want the latest calendar read", events[0].Value)
}
}
// A voided calendar fact is gone, not history to recite.
func TestCalendarEventsSkipsVoidedRows(t *testing.T) {
store := newTestStore(t)
defer store.Close()
ctx := context.Background()
day := time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC)
id, err := store.WriteFact(ctx, day.Add(14*time.Hour), KindEnv, "calendar_event_20260803_Cancelled",
`"Cancelled @ 14:00-14:30"`, calendar.SourcePersonal, 1.0, sql.NullInt64{})
if err != nil {
t.Fatalf("WriteFact: %v", err)
}
if _, err := store.WriteFact(ctx, day.Add(15*time.Hour), KindEnv, "calendar_event_20260803_Cancelled",
`"Cancelled @ 14:00-14:30"`, calendar.SourcePersonal, 1.0, sql.NullInt64{Int64: id, Valid: true}); err != nil {
t.Fatalf("WriteFact void: %v", err)
}
events, err := store.CalendarEvents(ctx, day, day.AddDate(0, 0, 1))
if err != nil {
t.Fatalf("CalendarEvents: %v", err)
}
for _, e := range events {
if e.ID == id {
t.Fatalf("voided row %d came back: %+v", id, e)
}
}
}
// The work calendar arrives as relayed phone notifications, not a CalDAV read
// (Vikunja #126). Those events belong in the same day's answer, and their
// provenance has to survive the query so the caller can hedge them.