da9114b623
Owner explicitly requested direct commits to master; bypass the branch-only hook.
131 lines
4.2 KiB
Go
131 lines
4.2 KiB
Go
package dialogue
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/store"
|
|
)
|
|
|
|
// openStore — a store on disk, so a second handle can reopen the same file.
|
|
func openStore(t *testing.T, path string) *store.Store {
|
|
t.Helper()
|
|
s, err := store.Open(context.Background(), path)
|
|
if err != nil {
|
|
t.Fatalf("open store: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = s.Close() })
|
|
return s
|
|
}
|
|
|
|
// A session written before a restart comes back and still merges a follow-up.
|
|
func TestSessionSurvivesRestart(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(t.TempDir(), "maven_test.db")
|
|
now := time.Now().UTC().Truncate(time.Millisecond)
|
|
|
|
first := openStore(t, path)
|
|
before := NewPersistentSessionStore(2*time.Minute, first)
|
|
before.Put("voice", &Session{
|
|
Intent: IntentReminder,
|
|
Slots: Slots{Text: "полить цветы", Time: now.Add(time.Hour), HasTime: true},
|
|
Utterance: "напомни полить цветы",
|
|
Conversational: true,
|
|
Timestamp: now,
|
|
TTL: 2 * time.Minute,
|
|
History: []Turn{{Intent: IntentChat, Text: "мы говорили о цветах"}},
|
|
})
|
|
if err := first.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
// fresh handle, fresh in-memory map — as after a daemon restart
|
|
after := openStore(t, path)
|
|
reloaded := NewPersistentSessionStore(2*time.Minute, after)
|
|
if err := reloaded.Load(ctx, now.Add(10*time.Second)); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
sess := reloaded.Get("voice", now.Add(10*time.Second))
|
|
if sess == nil {
|
|
t.Fatal("session did not survive the restart")
|
|
}
|
|
if sess.Intent != IntentReminder {
|
|
t.Fatalf("intent = %q, want reminder", sess.Intent)
|
|
}
|
|
if sess.Utterance != "напомни полить цветы" {
|
|
t.Fatalf("utterance = %q after restart", sess.Utterance)
|
|
}
|
|
if !sess.Conversational {
|
|
t.Fatal("conversation mode was lost across restart")
|
|
}
|
|
if len(sess.History) != 1 || sess.History[0].Text != "мы говорили о цветах" {
|
|
t.Fatalf("history after restart = %+v", sess.History)
|
|
}
|
|
// the follow-up carries no text of its own; it must inherit the old one
|
|
merged := InheritSlots(sess.Slots, Slots{Time: now.Add(2 * time.Hour), HasTime: true})
|
|
if merged.Text != "полить цветы" {
|
|
t.Fatalf("merged text = %q, want the earlier turn's text", merged.Text)
|
|
}
|
|
if !merged.Time.Equal(now.Add(2 * time.Hour)) {
|
|
t.Fatalf("merged time = %v, want the follow-up's time", merged.Time)
|
|
}
|
|
}
|
|
|
|
// A session past its TTL is dead: a restart must not bring it back.
|
|
func TestExpiredSessionNotResurrected(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(t.TempDir(), "maven_test.db")
|
|
now := time.Now().UTC().Truncate(time.Millisecond)
|
|
|
|
first := openStore(t, path)
|
|
before := NewPersistentSessionStore(2*time.Minute, first)
|
|
before.Put("voice", &Session{
|
|
Intent: IntentReminder,
|
|
Slots: Slots{Text: "полить цветы"},
|
|
Timestamp: now,
|
|
TTL: time.Minute,
|
|
})
|
|
if err := first.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
after := openStore(t, path)
|
|
reloaded := NewPersistentSessionStore(2*time.Minute, after)
|
|
later := now.Add(5 * time.Minute) // well past the 1-min TTL
|
|
if err := reloaded.Load(ctx, later); err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if sess := reloaded.Get("voice", later); sess != nil {
|
|
t.Fatalf("expired session came back: %+v", sess)
|
|
}
|
|
// and it is gone from the DB too, not just from memory
|
|
rows, err := after.LoadDialogueSessions(ctx, later)
|
|
if err != nil {
|
|
t.Fatalf("LoadDialogueSessions: %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Fatalf("expired row still in the DB: %+v", rows)
|
|
}
|
|
}
|
|
|
|
// Delete removes the row as well, so an ended conversation stays ended.
|
|
func TestDeleteRemovesPersistedSession(t *testing.T) {
|
|
ctx := context.Background()
|
|
path := filepath.Join(t.TempDir(), "maven_test.db")
|
|
now := time.Now().UTC().Truncate(time.Millisecond)
|
|
|
|
s := openStore(t, path)
|
|
ss := NewPersistentSessionStore(2*time.Minute, s)
|
|
ss.Put("voice", &Session{Intent: IntentChat, Timestamp: now, TTL: time.Minute})
|
|
ss.Delete("voice")
|
|
rows, err := s.LoadDialogueSessions(ctx, now)
|
|
if err != nil {
|
|
t.Fatalf("LoadDialogueSessions: %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Fatalf("row survived Delete: %+v", rows)
|
|
}
|
|
}
|