Files
Maven/internal/dialogue/clarify_test.go
T
kami 9145b83100 Add the clarify data layer: a parked question with one missing slot
The router can already say "I am not sure" (Decision.Clarify) but the daemon
had nowhere to keep the request while it asked. PendingQuestion holds the
original slots, ClarifyStore parks one per dialogue id with a 90s TTL, and
Answer fills only the slots that were missing so an answer can never rewrite
what she already understood. Logic that uses this comes next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:20:28 +04:00

94 lines
2.6 KiB
Go

package dialogue
import (
"testing"
"time"
)
var clarifyNow = time.Date(2026, 7, 31, 10, 0, 0, 0, time.UTC)
func TestPendingQuestionAnswerFillsOnlyMissing(t *testing.T) {
fireAt := clarifyNow.Add(time.Hour)
q := &PendingQuestion{
Intent: IntentReminder,
Slots: Slots{Key: "mom", HasKey: true, Text: "напомни позвонить маме"},
Missing: []Slot{SlotTime},
}
got := q.Answer("в 11", Slots{Time: fireAt, HasTime: true, Key: "other", HasKey: true})
if !got.HasTime || !got.Time.Equal(fireAt) {
t.Fatalf("missing time slot not filled: %+v", got)
}
if got.Key != "mom" {
t.Fatalf("answer overwrote a filled slot: key=%q", got.Key)
}
if got.Text != "напомни позвонить маме" {
t.Fatalf("answer overwrote the original text: %q", got.Text)
}
}
func TestPendingQuestionAnswerKeepsGapWhenAnswerIsEmpty(t *testing.T) {
q := &PendingQuestion{Intent: IntentReminder, Missing: []Slot{SlotTime}}
got := q.Answer("не знаю", Slots{})
if got.HasTime {
t.Fatal("empty answer must not fill the slot")
}
if len(StillMissing(q.Missing, got)) != 1 {
t.Fatal("StillMissing should report the unfilled slot")
}
}
func TestStillMissing(t *testing.T) {
cases := []struct {
name string
want []Slot
slots Slots
left int
}{
{"all filled", []Slot{SlotTime, SlotKey}, Slots{HasTime: true, HasKey: true}, 0},
{"time gap", []Slot{SlotTime}, Slots{HasKey: true}, 1},
{"fn gap", []Slot{SlotFn}, Slots{}, 1},
{"text filled", []Slot{SlotText}, Slots{Text: "hi"}, 0},
{"nothing wanted", nil, Slots{}, 0},
}
for _, tc := range cases {
if got := StillMissing(tc.want, tc.slots); len(got) != tc.left {
t.Errorf("%s: got %v, want %d left", tc.name, got, tc.left)
}
}
}
func TestClarifyStoreExpiry(t *testing.T) {
s := NewClarifyStore(90 * time.Second)
s.Put("voice", &PendingQuestion{Asked: clarifyNow, Missing: []Slot{SlotTime}})
if s.Get("voice", clarifyNow.Add(30*time.Second)) == nil {
t.Fatal("question inside the TTL should be live")
}
if s.Get("voice", clarifyNow.Add(2*time.Minute)) != nil {
t.Fatal("question past the TTL should be dropped")
}
if s.Get("voice", clarifyNow) != nil {
t.Fatal("an expired question must be deleted on read, not linger")
}
}
func TestClarifyStoreDefaultTTL(t *testing.T) {
s := NewClarifyStore(0)
q := &PendingQuestion{Asked: clarifyNow}
s.Put("voice", q)
if q.TTL != 90*time.Second {
t.Fatalf("default TTL not applied: %v", q.TTL)
}
}
func TestCanAskOnce(t *testing.T) {
q := &PendingQuestion{}
if !q.CanAsk() {
t.Fatal("a fresh question should be askable")
}
q.Attempts = MaxAttempts
if q.CanAsk() {
t.Fatal("she must not ask twice")
}
}