diff --git a/cmd/mavend/actions_reminder.go b/cmd/mavend/actions_reminder.go index a2c11a2..7ff97ed 100644 --- a/cmd/mavend/actions_reminder.go +++ b/cmd/mavend/actions_reminder.go @@ -2,8 +2,11 @@ package main import ( "context" + "fmt" "log" + "time" + "github.com/kami/maven/internal/lexicon" "github.com/kami/maven/internal/phraser" "github.com/kami/maven/internal/router" ) @@ -33,5 +36,24 @@ func (h *reactiveHandler) actionReminder(ctx context.Context, dec router.Decisio log.Printf("voice: create reminder: %v", err) return phraser.Ack(phraser.FailReminder, nil) } - return "" + // Phrased from the row, never from the utterance (Vikunja #507). The + // replier only ever saw Slots.Text, so it named whatever hour the sentence + // contained — including one the parser had rejected or read differently. + // A confirmation naming an hour no row holds is worse than a clarify, + // because he stops thinking about it. + return reminderConfirm(dec.Slots.Time, h.now()) +} + +// reminderConfirm — the confirmation for a reminder that exists, naming the +// stored fire time. Deterministic on purpose: the one sentence that must match +// a database row is not one to hand to a 1.7B. +func reminderConfirm(fire, now time.Time) string { + when := dayPrefix(now, fire) + if when == "это" { + // Further out than the day words reach — say the date instead of a + // word that would be wrong. + date := fmt.Sprintf("%d %s", fire.Day(), lexicon.MonthGenitive(int(fire.Month()))) + return "хорошо, напомню " + date + " в " + fire.Format("15:04") + "." + } + return "хорошо, напомню " + when + " в " + fire.Format("15:04") + "." } diff --git a/cmd/mavend/reactive_notes_test.go b/cmd/mavend/reactive_notes_test.go index 22bedc3..811e706 100644 --- a/cmd/mavend/reactive_notes_test.go +++ b/cmd/mavend/reactive_notes_test.go @@ -44,9 +44,12 @@ func TestReactiveNotesReminders(t *testing.T) { HasTime: true, }, } + // The confirmation is phrased from the row now (Vikunja #507), so it + // names the stored hour rather than leaving the replier to read one + // out of the sentence. reply := h.applyAction(ctx, dec) - if reply != "" { - t.Errorf("expected empty reply from applyAction, got %q", reply) + if want := "хорошо, напомню завтра в " + fireAt.Format("15:04") + "."; reply != want { + t.Errorf("reply = %q, want %q", reply, want) } reminders, err := st.ListReminders(ctx, 10) if err != nil { diff --git a/cmd/mavend/reminder_confirm_test.go b/cmd/mavend/reminder_confirm_test.go new file mode 100644 index 0000000..ade67c3 --- /dev/null +++ b/cmd/mavend/reminder_confirm_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "strings" + "testing" + "time" +) + +// A reminder confirmation is the one sentence that must match a database row. +// It used to be phrased by the replier from Slots.Text, which meant it named +// whatever hour the sentence contained — including an hour the parser had +// rejected or read differently (Vikunja #507). + +func TestReminderConfirmNamesTheStoredHour(t *testing.T) { + now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC) + got := reminderConfirm(now.Add(10*time.Hour), now) // 19:00 today + if !strings.Contains(got, "19:00") { + t.Fatalf("confirmation = %q, want the stored 19:00 in it", got) + } + if !strings.Contains(got, "сегодня") { + t.Fatalf("confirmation = %q, want it to say сегодня", got) + } +} + +func TestReminderConfirmUsesADateBeyondTheDayWords(t *testing.T) { + // dayPrefix answers "это" past послезавтра, and "напомню это в 09:00" is + // not a sentence. A date is. + now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC) + got := reminderConfirm(now.Add(10*24*time.Hour), now) + if strings.Contains(got, "это") { + t.Fatalf("confirmation = %q, want a date rather than the fallback day word", got) + } + if !strings.Contains(got, "15 августа") { + t.Fatalf("confirmation = %q, want the date in it", got) + } +} + +func TestReminderConfirmIsFeminineAndInformal(t *testing.T) { + // The persona checks the phrasing eval enforces apply here too, and this + // sentence never passes through a phraser. + now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC) + got := reminderConfirm(now.Add(time.Hour), now) + for _, bad := range []string{"вы", "ваш", "напомнил ", "рад "} { + if strings.Contains(strings.ToLower(got), bad) { + t.Fatalf("confirmation = %q contains %q", got, bad) + } + } + if !strings.HasPrefix(got, "хорошо, напомню") { + t.Fatalf("confirmation = %q, want it to open with the promise", got) + } +} diff --git a/cmd/mavend/ruwords.go b/cmd/mavend/ruwords.go index c254be8..0fe4a84 100644 --- a/cmd/mavend/ruwords.go +++ b/cmd/mavend/ruwords.go @@ -165,6 +165,8 @@ func formatTime(t time.Time) string { n := int(diff.Hours()) return fmt.Sprintf("%d %s назад", n, say.CountWord(n, "час", "часа", "часов")) default: - return t.Format("2 января 15:04") + // Not t.Format("2 января …"): Go reads that as a literal, so every + // fact older than a day used to read as January (Vikunja #507). + return fmt.Sprintf("%d %s %s", t.Day(), lexicon.MonthGenitive(int(t.Month())), t.Format("15:04")) } }