reminders: confirm from the row, not from the sentence (V-507)
The confirmation was phrased by the replier off Slots.Text, so it named
whatever hour the utterance contained — including one the parser rejected
or read differently. He heard 'напомню в семь' with no row at seven, and
stopped thinking about it.
actionReminder now phrases it itself from the stored fire time, so the
sentence and the row cannot disagree. Deterministic: the one sentence that
must match a database row is not one to hand to a 1.7B.
Also fixes formatTime, which had t.Format("2 января") — Go reads that as a
literal, so every fact older than a day read as January. The month comes
from internal/lexicon now, which is where months live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/lexicon"
|
||||||
"github.com/kami/maven/internal/phraser"
|
"github.com/kami/maven/internal/phraser"
|
||||||
"github.com/kami/maven/internal/router"
|
"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)
|
log.Printf("voice: create reminder: %v", err)
|
||||||
return phraser.Ack(phraser.FailReminder, nil)
|
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") + "."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,12 @@ func TestReactiveNotesReminders(t *testing.T) {
|
|||||||
HasTime: true,
|
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)
|
reply := h.applyAction(ctx, dec)
|
||||||
if reply != "" {
|
if want := "хорошо, напомню завтра в " + fireAt.Format("15:04") + "."; reply != want {
|
||||||
t.Errorf("expected empty reply from applyAction, got %q", reply)
|
t.Errorf("reply = %q, want %q", reply, want)
|
||||||
}
|
}
|
||||||
reminders, err := st.ListReminders(ctx, 10)
|
reminders, err := st.ListReminders(ctx, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,8 @@ func formatTime(t time.Time) string {
|
|||||||
n := int(diff.Hours())
|
n := int(diff.Hours())
|
||||||
return fmt.Sprintf("%d %s назад", n, say.CountWord(n, "час", "часа", "часов"))
|
return fmt.Sprintf("%d %s назад", n, say.CountWord(n, "час", "часа", "часов"))
|
||||||
default:
|
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user