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}, Timestamp: now, TTL: 2 * time.Minute, }) 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) } // 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) } }