Files
Maven/cmd/mavend/actions_query_continued_test.go
kami 9397f9e5f6 query: only a source that knows the day may answer "а завтра?"
The follow-up continuation re-aimed Slots.Time, but every query source
matches on dec.Utterance and nothing in the chain reads it. So the
inheritance bought almost nothing: "какие планы на сегодня" then "а
завтра?" missed day-plan's matcher and fell to the calendar, which had
parsed the day out of the raw utterance anyway.

Worse than nothing in one place. queryFactByKey runs first and claims on
HasKey plus HasTime, both of which the continuation sets, so a keyed
query continued with "а вчера?" answered "я записала это <the fact's own
timestamp>" and dropped the day entirely.

Widening the topic for every source would have made it worse still, not
better: CalendarEvents is the only CoreAPI call that takes a date, so
ten date-blind sources would have answered a question about tomorrow
with today's data. Gate instead of widen — a continuation is only
offered to a source that reads the day, and when none claims it she says
so rather than "не знаю", which reads as "nothing tomorrow" when she
never looked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
2026-08-01 22:32:37 +04:00

117 lines
3.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}