package main import ( "testing" "time" "github.com/kami/maven/internal/dialogue" "github.com/kami/maven/internal/router" ) var contNow = time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) func contSession(intent dialogue.Intent, key string) *dialogue.Session { return &dialogue.Session{ Intent: intent, Slots: dialogue.Slots{Key: key, HasKey: key != "", Text: "какие напоминания на сегодня"}, Timestamp: contNow.Add(-30 * time.Second), TTL: 2 * time.Minute, } } func TestContinuationInheritsTheQuestion(t *testing.T) { prev := contSession(dialogue.IntentQuery, "water") dec, ok := continuationDecision(prev, "а завтра?", contNow) if !ok { t.Fatal("continuationDecision returned false, want a decision") } if dec.Intent != router.IntentQuery { t.Errorf("intent = %q, want query", dec.Intent) } if dec.Slots.Key != "water" || !dec.Slots.HasKey { t.Errorf("key = %q, want water carried over", dec.Slots.Key) } if !dec.Slots.HasTime { t.Fatal("no time slot; the whole point is re-aiming the day") } if got, want := dec.Slots.Time.Format("2006-01-02"), "2026-08-02"; got != want { t.Errorf("time = %s, want %s", got, want) } } func TestContinuationAcceptsABareDate(t *testing.T) { prev := contSession(dialogue.IntentQuery, "water") for _, s := range []string{"завтра?", "вчера", "а вчера?", "и завтра"} { if _, ok := continuationDecision(prev, s, contNow); !ok { t.Errorf("continuationDecision(%q) = false, want true", s) } } } func TestContinuationDeclinesWhatIsNotAnEllipsis(t *testing.T) { prev := contSession(dialogue.IntentQuery, "water") for _, s := range []string{ // No date to re-aim at — an ordinary short utterance, the router's job. "а что там", "а бэкап?", "привет", "", // Content of its own: the verb is not an ellipsis. "напомни завтра позвонить маме", // Too long to be an ellipsis even with a date in it. "а что у меня стоит в календаре на завтра", } { if _, ok := continuationDecision(prev, s, contNow); ok { t.Errorf("continuationDecision(%q) = true, want false", s) } } } func TestContinuationDeclinesUncontinuableIntents(t *testing.T) { // act is the one that matters: inheriting an allowlisted fn from a // two-word utterance would be a way to run a destructive command. for _, in := range []dialogue.Intent{ dialogue.IntentAct, dialogue.IntentFact, dialogue.IntentNote, dialogue.IntentChat, } { if _, ok := continuationDecision(contSession(in, "water"), "а завтра?", contNow); ok { t.Errorf("continuationDecision inherited intent %q, want refusal", in) } } } func TestContinuationDeclinesWithoutALiveSession(t *testing.T) { if _, ok := continuationDecision(nil, "а завтра?", contNow); ok { t.Error("continued with no previous turn") } stale := contSession(dialogue.IntentQuery, "water") stale.Timestamp = contNow.Add(-10 * time.Minute) if _, ok := continuationDecision(stale, "а завтра?", contNow); ok { t.Error("continued an expired session") } } func TestContinuationNeverCarriesAnFn(t *testing.T) { prev := contSession(dialogue.IntentQuery, "water") prev.Slots.Fn, prev.Slots.HasFn = "restart", true dec, ok := continuationDecision(prev, "а завтра?", contNow) if !ok { t.Fatal("want a decision") } if dec.Slots.HasFn || dec.Slots.Fn != "" { t.Fatalf("carried fn %q into a continuation", dec.Slots.Fn) } }