9397f9e5f6
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
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
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)
|
||
}
|
||
}
|