7d08d27efb
An elliptical follow-up carries no intent of its own. followUpMerge cannot help — it inherits slots once the intent is known, and here the intent is the missing part. So "а завтра?" went to the router, which on a 1.7B is close to a coin flip, and the guess cost ~2.7s. continuationDecision runs before the router and rebuilds the turn from the previous one: same intent, same key, new day. Deterministic and free. Three guards, all narrow on purpose. A parseable date is required, which is what separates an ellipsis from an ordinary short utterance. Four tokens max. And only query, system and reminder may be inherited: fact and note would write something he did not say, and act would let a two-word utterance re-run an allowlisted fn, which is a way to fire a destructive command nobody typed. A continuation is still remembered, so "а завтра?" then "а послезавтра?" chains. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
101 lines
3.4 KiB
Go
101 lines
3.4 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.
|
||
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)
|
||
}
|
||
}
|