From 84ba217892d90138b3044b2c5a68f3f92681ac88 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 14:05:21 +0400 Subject: [PATCH] Say so when the day asked about is out of reach --- cmd/mavend/system_reply_test.go | 21 +++++++++++++++++++ cmd/mavend/voice.go | 36 ++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/cmd/mavend/system_reply_test.go b/cmd/mavend/system_reply_test.go index 1e8732e..d04fe54 100644 --- a/cmd/mavend/system_reply_test.go +++ b/cmd/mavend/system_reply_test.go @@ -35,6 +35,27 @@ func TestReplySystemDateOffset(t *testing.T) { } } +// A day she cannot work out must not come back as today's date — that is the +// same silent wrong answer #388 was about, one step further out. +func TestReplySystemUnknownDayIsHonest(t *testing.T) { + now := time.Date(2026, 7, 30, 14, 5, 0, 0, time.UTC) + h := systemHandler(now) + for _, u := range []string{ + "какое число в пятницу", + "какое число через неделю", + "какое число в понедельник", + } { + got := h.replySystem(context.Background(), router.Decision{Utterance: u}) + if got != onlyNearDaysReply { + t.Errorf("replySystem(%q) = %q, want the honest reply", u, got) + } + } + // The days she does know must not be caught by the same guard. + if got := h.replySystem(context.Background(), router.Decision{Utterance: "какое число завтра"}); got == onlyNearDaysReply { + t.Error("завтра was treated as an unknown day") + } +} + // TestReplySystemClockCity — the clock arm must not answer local time for a // question about another city (Vikunja #388). Known cities get their own zone; // unknown places get an honest "local time only". diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index 249fba6..feb0414 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -755,7 +755,9 @@ func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision) } // Calendar questions: "что у меня сегодня?", "планы на завтра?" - if date, ok := router.ParseCalendarDate(dec.Utterance, time.Now()); ok { + // h.now(), not time.Now(): the handler's clock is the injected one, so + // this arm can be tested at a fixed time like the rest. + if date, ok := router.ParseCalendarDate(dec.Utterance, h.now()); ok { events, err := h.api.CalendarEvents(ctx, date, date.Add(24*time.Hour)) if err != nil { log.Printf("voice: calendar events: %v", err) @@ -1021,6 +1023,33 @@ func mentionsUnknownPlace(u string) bool { return false } +// onlyNearDaysReply — she can work out today, tomorrow, the day after and +// yesterday, and nothing further. Said out loud instead of answering today's +// date for a day she did not understand. +const onlyNearDaysReply = "я считаю только сегодня, завтра, послезавтра и вчера — про другие дни пока не скажу." + +// dayWords — day references the calendar parser cannot resolve. A weekday name +// or a "через …" phrase means he asked about a specific other day. +var dayWords = []string{ + "понедельник", "вторник", "сред", "четверг", "пятниц", "суббот", "воскресен", + "через", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", +} + +// mentionsUnknownDay reports whether the question names a day the calendar +// parser could not resolve. Mirror of mentionsUnknownPlace: it exists only to +// pick an honest reply over a confidently wrong one. +// +// Only called after ParseCalendarDate has already failed, so "завтра" and the +// other words it does know never reach here. +func mentionsUnknownDay(u string) bool { + for _, w := range dayWords { + if strings.Contains(u, w) { + return true + } + } + return false +} + // ruClock renders the clock part of the time reply: "15 часов 4 минуты". func ruClock(t time.Time) string { h, m := t.Hour(), t.Minute() @@ -1148,6 +1177,11 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision) if d, ok := router.ParseCalendarDate(u, now); ok { day = d prefix = dayPrefix(now, d) + } else if mentionsUnknownDay(u) { + // He named a day she cannot work out ("в пятницу", "через неделю"). + // Answering today's date here would be the same silent wrong answer + // this arm was fixed for, so say what she can do instead. + return onlyNearDaysReply } dow := ruWeekdays[day.Weekday()] month := ruMonths[day.Month()-1]