Files
Maven/cmd/mavend/continuation_test.go
T
kami 3f98a99f44 continuation: only an ellipsis may widen the keyword match
Deployed check, second round: "привет" after "какой сегодня день" answered
with the date. followUpMerge fills an empty Text from the previous
same-intent turn, so the topic-widening added a minute earlier was reading
an inherited topic on turns that had nothing to do with it.

Decision gains Continued, set only by continuation.go and never by the
router. replySystem widens on that and nothing else, so an inherited Text
is back to being invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
2026-08-01 22:21:28 +04:00

141 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
// reminder is here because its payload is its Text, and the Text embeds
// the day word it was created with — see continuableIntents.
for _, in := range []dialogue.Intent{
dialogue.IntentAct, dialogue.IntentFact, dialogue.IntentNote,
dialogue.IntentChat, dialogue.IntentReminder,
} {
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)
}
}
// TestContinuationCarriesTheTopic — the ellipsis names the day; what he is
// asking ABOUT has to come from the previous turn, or replySystem keyword-
// matches "а завтра?" and finds nothing. Caught on the deployed daemon.
func TestContinuationCarriesTheTopic(t *testing.T) {
prev := contSession(dialogue.IntentSystem, "")
prev.Slots.Text = "какой сегодня день"
dec, ok := continuationDecision(prev, "а завтра?", contNow)
if !ok {
t.Fatal("want a decision")
}
if dec.Slots.Text != "какой сегодня день" {
t.Fatalf("Slots.Text = %q, want the previous turn's topic", dec.Slots.Text)
}
}
// TestReplySystemIgnoresAnInheritedTopic — the regression the deployed daemon
// showed on 01-08-2026: followUpMerge fills an empty Text from the previous
// same-intent turn, so a plain "привет" after "какой сегодня день" arrived at
// replySystem carrying the old topic and was answered with the date. Only a
// continuation may widen the keyword match.
func TestReplySystemIgnoresAnInheritedTopic(t *testing.T) {
h := &reactiveHandler{now: func() time.Time { return contNow }}
inherited := router.Decision{
Utterance: "привет",
Intent: router.IntentSystem,
Slots: router.Slots{Text: "какой сегодня день"},
}
if got := h.replySystem(nil, inherited); got != "пока не умею отвечать на этот вопрос." {
t.Fatalf("replySystem answered %q on an inherited topic", got)
}
cont := inherited
cont.Utterance, cont.Continued = "а завтра?", true
if got := h.replySystem(nil, cont); got == "пока не умею отвечать на этот вопрос." {
t.Fatalf("replySystem refused a real continuation")
}
}