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) } }) // --- Cross-intent + anaphora tests (P3.2) --- t.Run("query after fact inherits key via anaphora", func(t *testing.T) { prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentQuery, Utterance: "когда я это сделал?", } got := followUpMerge(prior, cur, base.Add(30*time.Second)) if !got.Slots.HasKey { t.Error("query after fact with anaphora: key not inherited") } if got.Slots.Key != "water" { t.Errorf("query after fact: got key=%q, want water", got.Slots.Key) } }) t.Run("query after fact without anaphora does not inherit", func(t *testing.T) { prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentQuery, Utterance: "какая погода в москве?", } got := followUpMerge(prior, cur, base.Add(30*time.Second)) if got.Slots.HasKey { t.Error("query without anaphora inherited key when it shouldn't") } }) t.Run("three-turn breaks context correctly", func(t *testing.T) { // Simulate: turn 1 (fact: water), turn 2 (weather query — break), // turn 3 (query referring to turn 1 should NOT inherit from turn 2). turn2 := &dialogue.Session{ Intent: dialogue.IntentQuery, Slots: dialogue.Slots{Text: "какая погода в москве?"}, Timestamp: base.Add(30 * time.Second), TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentQuery, Utterance: "когда я это сделал?", } // turn2 is the "prior" but has no key — anaphora should not resolve. got := followUpMerge(turn2, cur, base.Add(60*time.Second)) if got.Slots.HasKey { t.Error("key inherited across a weather break that had no key") } }) t.Run("anaphora in reminder inherits key", func(t *testing.T) { prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentReminder, Utterance: "напомни про это завтра", } got := followUpMerge(prior, cur, base.Add(30*time.Second)) if !got.Slots.HasKey { t.Error("reminder with anaphora: key not inherited") } if got.Slots.Key != "water" { t.Errorf("reminder anaphora: got key=%q, want water", got.Slots.Key) } }) t.Run("anaphora in new fact inherits key", func(t *testing.T) { prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentFact, Slots: router.Slots{Value: "2 литра"}, Utterance: "я выпил это", } got := followUpMerge(prior, cur, base.Add(30*time.Second)) if !got.Slots.HasKey { t.Error("fact with anaphora: key not inherited") } if got.Slots.Key != "water" { t.Errorf("fact anaphora: got key=%q, want water", got.Slots.Key) } }) t.Run("explicit key wins over anaphora", func(t *testing.T) { prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentFact, Slots: router.Slots{Key: "sleep", HasKey: true, Value: "6h"}, Utterance: "я спал 6 часов", } // Even though the utterance doesn't have anaphora, the explicit key // from the fact parser should win — same intent, same merge as before. got := followUpMerge(prior, cur, base.Add(30*time.Second)) if !got.Slots.HasKey || got.Slots.Key != "sleep" { t.Errorf("explicit key overwritten by prior: got key=%q", got.Slots.Key) } }) t.Run("query after fact inherits time too", func(t *testing.T) { factTime := base.Add(-2 * time.Hour) prior := &dialogue.Session{ Intent: dialogue.IntentFact, Slots: dialogue.Slots{Key: "water", HasKey: true, Time: factTime, HasTime: true}, Timestamp: base, TTL: 2 * time.Minute, } cur := router.Decision{ Intent: router.IntentQuery, Utterance: "когда я это сделал?", } got := followUpMerge(prior, cur, base.Add(30*time.Second)) if !got.Slots.HasTime || !got.Slots.Time.Equal(factTime) { t.Errorf("query after fact did not inherit time: HasTime=%v, Time=%v", got.Slots.HasTime, got.Slots.Time) } }) }