package router import ( "context" "testing" "time" ) func TestMentionsTime(t *testing.T) { for _, s := range []string{ "напомни в семь вечера позвонить маме", "напомни в 19:30 позвонить маме", "напомни без четверти восемь выходить", "напомни в половине первого пообедать", "разбуди меня полвосьмого", "напомни завтра принять лекарство", "напомни в пятницу забрать заказ", "напомни через двадцать минут", "напомни утром выпить таблетку", "remind me at noon to stretch", } { if !MentionsTime(s) { t.Errorf("MentionsTime(%q) = false; this sentence names a time", s) } } } // TestNamesAnHour — the gate on the reminder's time slot (V-577, V-579). A day // word is not an hour, and a sentence that names no hour is asked about rather // than completed from the clock. func TestNamesAnHour(t *testing.T) { for _, s := range []string{ "в 11:00", "на 9", "в 9", "в девять", "в 9 утра", "завтра в 9", "через час", "через двадцать минут", "в половине восьмого", "без четверти восемь", "remind me at noon", "вечером", } { if !NamesAnHour(s) { t.Errorf("NamesAnHour(%q) = false; this names an hour or an interval", s) } } for _, s := range []string{ "на завтра", "что у меня сегодня?", "напомни завтра позвонить маме", "в пятницу", "позвонить маме", "", } { if NamesAnHour(s) { t.Errorf("NamesAnHour(%q) = true; no hour was spoken, so she has to ask", s) } } } // TestHourIsAmbiguous — the owner's rule of 2026-08-06. A bare hour is either // half of the day and gets asked about; a qualifier, a written clock, an hour // above twelve or an interval settles it and goes straight through. func TestHourIsAmbiguous(t *testing.T) { for _, s := range []string{ "напомни завтра в 3 заказать цветы", "в 9", "на 9", "в девять", "в 11 позвонить маме", } { if !HourIsAmbiguous(s) { t.Errorf("HourIsAmbiguous(%q) = false; the hour could be either half of the day", s) } } for _, s := range []string{ "напомни в 9 вечера разгрузить стиралку", "завтра в 15:00", "в 21", "в 11:00", "через час", "через 10 минут", "remind me at noon", "напомни позвонить маме", } { if HourIsAmbiguous(s) { t.Errorf("HourIsAmbiguous(%q) = true; this time reads only one way", s) } } } // TestNamesAnInterval — an interval resolves to one instant, so it answers the // hour and the day at once and is never asked about. func TestNamesAnInterval(t *testing.T) { for _, s := range []string{"через час", "через 10 минут", "через полчаса", "in 30 minutes"} { if !NamesAnInterval(s) { t.Errorf("NamesAnInterval(%q) = false", s) } } for _, s := range []string{"завтра в 15:00", "в 9 вечера", ""} { if NamesAnInterval(s) { t.Errorf("NamesAnInterval(%q) = true", s) } } } // TestReminderSlotRefusesAnHourNobodySaid — the same rule where it bites. The // parser answers a bare day word with that day at the current minute, and the // slot must stay empty so the daemon asks. func TestReminderSlotRefusesAnHourNobodySaid(t *testing.T) { now := time.Date(2026, 8, 6, 1, 38, 0, 0, time.UTC) ex := Extractor{Time: clockEchoParser{}} if got := ex.Extract(context.Background(), IntentReminder, "на завтра", now); got.HasTime { t.Errorf("«на завтра» filled the time slot with %s, which is the clock", got.Time.Format("15:04")) } // "на 9" names an hour, and a parser that answered with the clock did not // read it (V-610). Naming one is necessary and reading it is what fills the // slot, so this echo is refused too and the daemon asks. if got := ex.Extract(context.Background(), IntentReminder, "на 9", now); got.HasTime { t.Errorf("«на 9» took %s from the clock; the parser never read the nine", got.Time.Format("15:04")) } // A parser that does read it fills the slot, which is the other half of the // same rule. real := Extractor{Time: StubDateTimeParser{}} if got := real.Extract(context.Background(), IntentReminder, "на 9", now); !got.HasTime || got.Time.Hour() != 9 { t.Errorf("«на 9» must fill the slot with nine o'clock, got %+v", got) } } // clockEchoParser stands in for what both real parsers do with a bare day word: // it answers with the current time of day. type clockEchoParser struct{} func (clockEchoParser) Parse(_ context.Context, _ string, now time.Time) (time.Time, bool, error) { return now.AddDate(0, 0, 1), true, nil } // A sentence with no time in it must not read as one, or a real follow-up stops // inheriting the hour it meant. func TestMentionsTimeIgnoresSentencesWithoutOne(t *testing.T) { for _, s := range []string{ "напомни позвонить маме", "и ещё полить цветы", "напомни про счёт за свет", "купить три яблока", "перезапусти докер", "счёт 3:2 в нашу пользу", "", } { if MentionsTime(s) { t.Errorf("MentionsTime(%q) = true; there is no time in it", s) } } }