package main import ( "context" "strings" "testing" "time" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/router" ) // contQueryAPI records which core call a continued query reached. DayPlan and // LatestFact are here to be caught, not to be used: a continuation must never // reach them, and the counters are how the test says so. type contQueryAPI struct { ipc.UnimplementedCoreAPI from, to time.Time events int plans int factLooks int } func (a *contQueryAPI) CalendarEvents(_ context.Context, from, to time.Time) ([]ipc.Fact, error) { a.events++ a.from, a.to = from, to return []ipc.Fact{{Key: "calendar", Value: "Планёрка @ 14:00", Confidence: 1.0, Ts: from.Add(14 * time.Hour)}}, nil } func (a *contQueryAPI) DayPlan(context.Context) (ipc.DayPlan, error) { a.plans++ return ipc.DayPlan{Spoken: "план на сегодня"}, nil } func (a *contQueryAPI) LatestFact(_ context.Context, key string) (ipc.Fact, error) { a.factLooks++ return ipc.Fact{Key: key, Value: "2л", Ts: contNow.Add(-time.Hour)}, nil } func contQueryHandler() (*reactiveHandler, *contQueryAPI) { api := &contQueryAPI{} return &reactiveHandler{api: api, now: func() time.Time { return contNow }}, api } // A continuation is a question about another day, so the one source that can // read a day answers it — for the day the ellipsis named, not for today. func TestContinuedQueryReachesTheCalendar(t *testing.T) { h, api := contQueryHandler() reply := h.actionQuery(context.Background(), router.Decision{ Intent: router.IntentQuery, Utterance: "а завтра?", Continued: true, Slots: router.Slots{Text: "что у меня сегодня", Time: contNow.Add(24 * time.Hour), HasTime: true}, }) if api.events != 1 { t.Fatalf("CalendarEvents called %d times, want 1", api.events) } if got, want := api.from.Format("2006-01-02"), "2026-08-02"; got != want { t.Errorf("asked the calendar for %s, want %s", got, want) } if reply == "" { t.Error("empty reply") } } // The regression this gate exists for: every other source is date-blind, so // letting one claim a continuation answers a question about tomorrow with // today's data. queryFactByKey was the live case — HasKey plus HasTime, both // set by the continuation, and it replies with a stored fact's own timestamp. func TestContinuedQuerySkipsDateBlindSources(t *testing.T) { h, api := contQueryHandler() h.actionQuery(context.Background(), router.Decision{ Intent: router.IntentQuery, Utterance: "а вчера?", Continued: true, Slots: router.Slots{ Key: "water", HasKey: true, Text: "когда я пил воду", Time: contNow.Add(-24 * time.Hour), HasTime: true, }, }) if api.factLooks != 0 { t.Errorf("fact-by-key claimed a continuation (%d lookups)", api.factLooks) } if api.plans != 0 { t.Errorf("day-plan claimed a continuation (%d calls)", api.plans) } } // Nothing date-aware claimed it: say that, rather than "не знаю", which reads // as "no data for that day" when she never looked. func TestContinuedQueryWithNoDateAwareAnswerSaysSo(t *testing.T) { h, _ := contQueryHandler() // No parseable day in the utterance, so even the calendar passes. reply := h.actionQuery(context.Background(), router.Decision{ Intent: router.IntentQuery, Utterance: "а?", Continued: true, Slots: router.Slots{Text: "какая погода", HasTime: true}, }) if reply == "не знаю." || !strings.Contains(reply, "спроси целиком") { t.Fatalf("reply = %q, want the honest continuation refusal", reply) } } // An ordinary query is untouched by the gate — every source still runs. func TestOrdinaryQueryStillReachesEverySource(t *testing.T) { h, api := contQueryHandler() h.actionQuery(context.Background(), router.Decision{ Intent: router.IntentQuery, Utterance: "какие планы на сегодня?", }) if api.plans != 1 { t.Fatalf("day-plan called %d times on an ordinary query, want 1", api.plans) } }