53616836db
Deployed check: "какой сегодня день" then "а завтра?" answered "пока не умею". The intent was inherited correctly, but replySystem keyword-matches the utterance, and "а завтра?" contains no topic word — that is the whole nature of an ellipsis. So the topic travels with the session. rememberTurn keeps the raw utterance in Slots.Text for system and query turns that have no Text slot of their own (a stage-0 grammar fills none), and replySystem matches keywords against utterance + Slots.Text. Dates keep parsing from the utterance alone, which is the part the ellipsis actually restates. Only system and query: everywhere else Text is a payload and must stay exactly what the router put in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
119 lines
4.2 KiB
Go
119 lines
4.2 KiB
Go
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)
|
||
}
|
||
}
|