Files
Maven/cmd/mavend/followup_test.go
T
kami 388d4257ee maven: wire dialogue slot carry-over into the voice path (task 6)
The dialogue library (internal/dialogue, task 6) shipped tested but unwired.
Wire it: reactiveHandler now holds a 2-min SessionStore, and each turn fills
its missing slots from a prior same-intent, non-expired turn via InheritSlots
before acting, then records itself for the next follow-up. Single-user box →
one session slot (voiceDialogueID).

Guardrails (followUpMerge, unit-tested): only same-intent turns inherit (a new
intent is a fresh command); clarify turns and expired/nil priors never inherit;
InheritSlots fills gaps only, so a fully-slotted turn is untouched; the fact
Value (router-only) survives the dialogue.Slots round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 12:15:17 +04:00

99 lines
3.1 KiB
Go

package main
import (
"testing"
"time"
"github.com/kami/maven/internal/dialogue"
"github.com/kami/maven/internal/router"
)
func TestFollowUpMerge(t *testing.T) {
base := time.Date(2026, 7, 6, 12, 0, 0, 0, time.UTC)
fireAt := base.Add(24 * time.Hour)
// prior turn: a reminder that resolved a fire time.
prev := &dialogue.Session{
Intent: dialogue.IntentReminder,
Slots: dialogue.Slots{Time: fireAt, HasTime: true, Text: "старый текст"},
Timestamp: base,
TTL: 2 * time.Minute,
}
t.Run("same intent inherits the missing time", func(t *testing.T) {
// follow-up reminder with text but no parsed time.
cur := router.Decision{
Intent: router.IntentReminder,
Slots: router.Slots{Text: "позвонить маме"},
}
got := followUpMerge(prev, cur, base.Add(30*time.Second))
if !got.Slots.HasTime || !got.Slots.Time.Equal(fireAt) {
t.Errorf("time not inherited: HasTime=%v Time=%v", got.Slots.HasTime, got.Slots.Time)
}
if got.Slots.Text != "позвонить маме" {
t.Errorf("current text was overwritten: %q", got.Slots.Text)
}
})
t.Run("current slot wins over prior (gaps only)", func(t *testing.T) {
own := base.Add(48 * time.Hour)
cur := router.Decision{
Intent: router.IntentReminder,
Slots: router.Slots{Time: own, HasTime: true},
}
got := followUpMerge(prev, cur, base.Add(30*time.Second))
if !got.Slots.Time.Equal(own) {
t.Errorf("current time clobbered by prior: %v", got.Slots.Time)
}
})
t.Run("different intent does not inherit", func(t *testing.T) {
cur := router.Decision{Intent: router.IntentFact, Slots: router.Slots{Key: "water", HasKey: true}}
got := followUpMerge(prev, cur, base.Add(30*time.Second))
if got.Slots.HasTime {
t.Error("time bled across a different intent")
}
})
t.Run("clarify turn does not inherit", func(t *testing.T) {
cur := router.Decision{Intent: router.IntentReminder, Clarify: true}
got := followUpMerge(prev, cur, base.Add(30*time.Second))
if got.Slots.HasTime {
t.Error("clarify turn inherited slots")
}
})
t.Run("expired prior does not inherit", func(t *testing.T) {
cur := router.Decision{Intent: router.IntentReminder, Slots: router.Slots{Text: "x"}}
got := followUpMerge(prev, cur, base.Add(3*time.Minute)) // past the 2-min TTL
if got.Slots.HasTime {
t.Error("expired session still inherited")
}
})
t.Run("nil prior is a no-op", func(t *testing.T) {
cur := router.Decision{Intent: router.IntentReminder, Slots: router.Slots{Text: "x"}}
got := followUpMerge(nil, cur, base)
if got.Slots.HasTime {
t.Error("nil prior produced inheritance")
}
})
t.Run("router-only Value survives the round-trip", func(t *testing.T) {
cur := router.Decision{
Intent: router.IntentFact,
Slots: router.Slots{Key: "sleep", HasKey: true, Value: "6h"},
}
factPrev := &dialogue.Session{
Intent: dialogue.IntentFact,
Slots: dialogue.Slots{Text: "спал"},
Timestamp: base,
TTL: 2 * time.Minute,
}
got := followUpMerge(factPrev, cur, base.Add(10*time.Second))
if got.Slots.Value != "6h" {
t.Errorf("Value lost through dialogue conversion: %q", got.Slots.Value)
}
})
}