1d10c9535c
"напомни завтра к трём часам дня позвонить врачу" now sets 15:00. It set 03:53, which was the clock at the moment of the turn. She confirmed that as the hour he had just said. #252 taught hourPrepositions and the dateparser rewrite the preposition "к". So HasTime and NamesAnHour started answering true for the sentence. The value did not follow. The rewrite kept his preposition and handed dateparser "завтра к 03:00 pm". dateparser joins a day word to a clock through "в" and through no other Russian preposition. It read the day, dropped the clock and filled the time from its relative base. The completeness rule then saw what, time and day all answered, and committed at the current minute. The preposition is normalised along with the hour now. "на" was losing the clock the same way and was never measured. So "напомни завтра на 9" was landing on the current minute too. The second half is the durable one. ResolvedTheHour is the gate the reminder slot reads, and it refuses a parse whose minute nobody spoke. A spoken hour lands on the hour. The three shapes that name a minute of their own are a written clock, a half hour and a quarter to. Anything else came off the clock the parser was handed. An interval is exempt, because it lands where the arithmetic says. Comparing the whole instant to now is the obvious test and it is wrong. ru-rem-006 resolves to 12:00 and the fixture reference clock is 12:00. That is an hour he did say, reading as an hour nobody did. The five sentences measured on the box are pinned as tests. They run against the stub and against the production parser, and the two that already passed are in there too. Fixture unchanged. classifier+hash is 27/91 and classifier+onnx is 64/91, before and after. reach is 18/30 and 27/30, before and after. No case moved and no clarify count changed. Suite green under -race. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
5.4 KiB
Go
135 lines
5.4 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"))
|
|
}
|
|
// "на 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)
|
|
}
|
|
}
|
|
}
|