01c78ef369
Both parsers answer a bare day word with that day at the current minute, so "на завтра" set a reminder at 01:38, the minute he happened to be speaking. The gate is textual now: NamesAnHour reads the sentence, and the slot stays empty when nobody said an hour. Beside it, NamesAnInterval and HourIsAmbiguous, which the owner's commit rule reads. "на" joins "в" as a frame around a spoken hour in both parsers, a clock keeps its meaning with a full stop after it, and the stub applies a day word and a part-of-day qualifier from anywhere in the sentence rather than only from the token after the hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
4.8 KiB
Go
126 lines
4.8 KiB
Go
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"))
|
|
}
|
|
if got := ex.Extract(context.Background(), IntentReminder, "на 9", now); !got.HasTime {
|
|
t.Error("«на 9» names an hour and must still fill the slot")
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|