calendar: date an ambient event by its day word, and refuse stale ones
EventFromNotification took the date from the notification's own day, on the grounds that a meeting notification is about today or it would not be firing. Calendar apps break that. A 21:00 reminder reading "Tomorrow at 09:00" became an event at 09:00 today, twelve hours in the past, and FactKey filed that wrong meeting under today's date. Storing a wrong meeting is the one outcome this parse works to avoid. An explicit day word now moves the date: завтра, tomorrow, послезавтра, сегодня, today, tonight. Matched whole, so послезавтра is not read as завтра, and stripped from the summary so the meeting is not named after the day. Anything still landing more than two hours before the notification is refused, which covers the cases with no day word at all. The grace keeps a repost for a meeting already under way. Also matches the bearer scheme with EqualFold. A phone sending "bearer <tok>" fell through to the X-Maven-Token branch and got a 401 that looked like a wrong token. A bare token with no scheme in Authorization is now rejected rather than silently accepted. The route table in mavweb gains its /api/ambient row, and the missing calendar_busy write is recorded as a known gap. Found in review of #57.
This commit is contained in:
@@ -110,6 +110,97 @@ func TestEventFromNotification(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A notification is not always about today. A 21:00 reminder reading
|
||||
// "Tomorrow at 09:00" used to be dated to the notification's own day, which put
|
||||
// the meeting twelve hours in the past and filed it under today in FactKey. A
|
||||
// wrong meeting stored is worse than nothing stored.
|
||||
func TestEventFromNotificationDayWords(t *testing.T) {
|
||||
loc := time.FixedZone("+04", 4*3600)
|
||||
evening := time.Date(2026, 8, 3, 21, 0, 0, 0, loc)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
title, text string
|
||||
posted time.Time
|
||||
wantOK bool
|
||||
wantDay int // day of month
|
||||
wantSummary string
|
||||
}{
|
||||
{
|
||||
name: "tomorrow in english", title: "Standup", text: "Tomorrow at 09:00",
|
||||
posted: evening, wantOK: true, wantDay: 4, wantSummary: "Standup",
|
||||
},
|
||||
{
|
||||
name: "завтра in russian", title: "Планёрка", text: "завтра в 09:00",
|
||||
posted: evening, wantOK: true, wantDay: 4, wantSummary: "Планёрка",
|
||||
},
|
||||
{
|
||||
name: "завтра in the title, summary in the body", title: "Завтра в 09:00", text: "Планёрка",
|
||||
posted: evening, wantOK: true, wantDay: 4, wantSummary: "Планёрка",
|
||||
},
|
||||
{
|
||||
name: "послезавтра is two days, not one", title: "Ретро", text: "послезавтра 11:00",
|
||||
posted: evening, wantOK: true, wantDay: 5, wantSummary: "Ретро",
|
||||
},
|
||||
{
|
||||
name: "сегодня stays on the posted day", title: "Созвон", text: "сегодня 21:30",
|
||||
posted: evening, wantOK: true, wantDay: 3, wantSummary: "Созвон",
|
||||
},
|
||||
// No day word: the 09:00 is twelve hours behind the notification, so the
|
||||
// inferred day is wrong and there is nothing honest to store.
|
||||
{
|
||||
name: "stale morning time with no day word", title: "Standup", text: "at 09:00",
|
||||
posted: evening, wantOK: false,
|
||||
},
|
||||
// Inside the grace: a phone reposting the notification for a meeting
|
||||
// already under way must still store it.
|
||||
{
|
||||
name: "meeting already running", title: "Планёрка", text: "20:30-22:00",
|
||||
posted: evening, wantOK: true, wantDay: 3, wantSummary: "Планёрка",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ev, ok := EventFromNotification(Notification{
|
||||
Package: "com.google.android.calendar",
|
||||
Title: tt.title, Text: tt.text, Posted: tt.posted,
|
||||
})
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("ok = %v, want %v (event %+v)", ok, tt.wantOK, ev)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if got := ev.Start.Day(); got != tt.wantDay {
|
||||
t.Errorf("start day = %d, want %d (start %v)", got, tt.wantDay, ev.Start)
|
||||
}
|
||||
if ev.Summary != tt.wantSummary {
|
||||
t.Errorf("summary = %q, want %q", ev.Summary, tt.wantSummary)
|
||||
}
|
||||
if ev.Start.Before(tt.posted.Add(-ambientPastGrace)) {
|
||||
t.Errorf("start %v is stale against posted %v", ev.Start, tt.posted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The day word named the date, which now lives in Start. Leaving it in the
|
||||
// summary makes "Завтра Планёрка" the name of the meeting, and FactKey folds
|
||||
// that into the key.
|
||||
func TestEventFromNotificationDropsDayWordFromSummary(t *testing.T) {
|
||||
ev, ok := EventFromNotification(Notification{
|
||||
Title: "Завтра Планёрка 09:00",
|
||||
Posted: time.Date(2026, 8, 3, 21, 0, 0, 0, time.UTC),
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("expected an event")
|
||||
}
|
||||
if ev.Summary != "Планёрка" {
|
||||
t.Fatalf("summary = %q, want %q", ev.Summary, "Планёрка")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventFromNotificationNeedsPostedAt(t *testing.T) {
|
||||
if _, ok := EventFromNotification(Notification{Title: "Планёрка 10:00"}); ok {
|
||||
t.Error("a notification with no posted_at has no date to sit on")
|
||||
|
||||
Reference in New Issue
Block a user