a8710c859b
"что ты записала сегодня?" was recognised as a history question and then answered with "ты говорил: …". The rows are right — a tapped fact is one act seen from two sides — but the sentence hands the question back instead of answering it. historyAsks returns which side was asked and queryHistory phrases from it, including the nothing-found reply. His side is tested first, because "отмечать" is on both verb lists and "что я отметил" is not a question about her. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
143 lines
5.3 KiB
Go
143 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// historyAPI serves a fixed set of recent facts.
|
|
type historyAPI struct {
|
|
ipc.UnimplementedCoreAPI
|
|
facts []ipc.Fact
|
|
calls int
|
|
}
|
|
|
|
func (a *historyAPI) RecentFacts(context.Context, int) ([]ipc.Fact, error) {
|
|
a.calls++
|
|
return a.facts, nil
|
|
}
|
|
|
|
func historyHandler(now time.Time, facts ...ipc.Fact) (*reactiveHandler, *historyAPI) {
|
|
api := &historyAPI{facts: facts}
|
|
return &reactiveHandler{api: api, now: func() time.Time { return now }}, api
|
|
}
|
|
|
|
func askHistory(h *reactiveHandler, u string) (string, bool) {
|
|
return h.queryHistory(context.Background(), &queryTurn{
|
|
dec: router.Decision{Intent: router.IntentQuery, Utterance: u},
|
|
})
|
|
}
|
|
|
|
func TestIsHistoryQuery(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
text string
|
|
want bool
|
|
}{
|
|
{"что я тебе говорил?", true},
|
|
{"что ты записала сегодня?", true},
|
|
{"что я отмечал?", true},
|
|
// Forms the truncated prefixes did not reach. The dictionary answers
|
|
// these because it lemmatises both sides (V-530).
|
|
{"что я тебе рассказывал?", true},
|
|
{"что я сказала вчера", true},
|
|
{"что ты запомнила?", true},
|
|
// The noun, not the verb. "рассказ" was a prefix of the old pair, so
|
|
// this read as a history question — the same defect V-528 fixed in
|
|
// complaint.go, where "лаг" matched "лагерь".
|
|
{"что я читал рассказ", false},
|
|
// A verb of saying with nobody saying it.
|
|
{"что записать?", false},
|
|
// A named topic is a recall question, and the notes pass answers it
|
|
// better than a list of the last five facts does.
|
|
{"что я говорил про сервер?", false},
|
|
{"что у меня сегодня?", false},
|
|
{"", false},
|
|
} {
|
|
if got := isHistoryQuery(tc.text); got != tc.want {
|
|
t.Errorf("isHistoryQuery(%q) = %v, want %v", tc.text, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHistoryReadsOnlyWhatHeSaid(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 20, 0, 0, 0, time.UTC)
|
|
h, api := historyHandler(now,
|
|
ipc.Fact{Key: "water", Value: "выпил", Source: "tap:voice", Ts: now.Add(-time.Hour)},
|
|
// Learned, not said: a poller writing this back under "что я тебе
|
|
// говорил?" would put words in his mouth.
|
|
ipc.Fact{Key: "spent_today", Value: "1200", Source: "poll:zenmoney", Ts: now.Add(-time.Hour)},
|
|
// Older than the window.
|
|
ipc.Fact{Key: "shower", Value: "принял", Source: "tap:voice", Ts: now.Add(-30 * time.Hour)},
|
|
)
|
|
reply, ok := askHistory(h, "что я тебе говорил?")
|
|
if !ok {
|
|
t.Fatal("the history question must be claimed before the recall sources")
|
|
}
|
|
if !strings.Contains(reply, "water") {
|
|
t.Errorf("reply = %q, want the fact he tapped in", reply)
|
|
}
|
|
if strings.Contains(reply, "spent_today") || strings.Contains(reply, "shower") {
|
|
t.Errorf("reply = %q, want only what he said inside the window", reply)
|
|
}
|
|
if api.calls != 1 {
|
|
t.Errorf("RecentFacts called %d times, want 1", api.calls)
|
|
}
|
|
}
|
|
|
|
// The rows are the same either way, because a tapped fact is one act seen from
|
|
// two sides. The sentence is not: "что ты записала" answered with "ты говорил"
|
|
// hands the question back (Vikunja #456).
|
|
func TestHistoryAnswersTheSideItWasAsked(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 20, 0, 0, 0, time.UTC)
|
|
h, _ := historyHandler(now, ipc.Fact{Key: "water", Value: "выпил", Source: "tap:voice", Ts: now.Add(-time.Hour)})
|
|
|
|
his, ok := askHistory(h, "что я тебе говорил?")
|
|
if !ok || !strings.HasPrefix(his, "ты говорил") {
|
|
t.Errorf("reply = %q, ok = %v, want his side", his, ok)
|
|
}
|
|
hers, ok := askHistory(h, "что ты записала сегодня?")
|
|
if !ok || !strings.HasPrefix(hers, "я записала") {
|
|
t.Errorf("reply = %q, ok = %v, want her side", hers, ok)
|
|
}
|
|
// "отмечать" is on both verb lists, so his subject has to win.
|
|
if side, ok := historyAsks("что я отметил?"); !ok || side != historyAskedHim {
|
|
t.Errorf("historyAsks(что я отметил) = %v, %v", side, ok)
|
|
}
|
|
|
|
empty, _ := historyHandler(now)
|
|
none, ok := askHistory(empty, "что ты записала сегодня?")
|
|
if !ok || !strings.Contains(none, "не записывала") {
|
|
t.Errorf("empty reply = %q, ok = %v, want her side", none, ok)
|
|
}
|
|
}
|
|
|
|
// Nothing said is an answer of its own. Falling through would hand the question
|
|
// to recall, which answers it with an old note.
|
|
func TestHistorySaysWhenThereIsNothing(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 20, 0, 0, 0, time.UTC)
|
|
h, _ := historyHandler(now)
|
|
reply, ok := askHistory(h, "что я тебе говорил?")
|
|
if !ok || !strings.Contains(reply, "ничего") {
|
|
t.Fatalf("reply = %q, ok = %v", reply, ok)
|
|
}
|
|
}
|
|
|
|
// Five is what fits in one spoken breath; the rest are on /history.
|
|
func TestHistoryStopsAtFive(t *testing.T) {
|
|
now := time.Date(2026, 8, 4, 20, 0, 0, 0, time.UTC)
|
|
var facts []ipc.Fact
|
|
for i := 0; i < 12; i++ {
|
|
facts = append(facts, ipc.Fact{Key: "k", Value: "v", Source: "tap:voice", Ts: now.Add(-time.Minute)})
|
|
}
|
|
h, _ := historyHandler(now, facts...)
|
|
reply, _ := askHistory(h, "что ты записала?")
|
|
if got := strings.Count(reply, ";"); got != historyReadOut-1 {
|
|
t.Fatalf("reply = %q has %d separators, want %d", reply, got, historyReadOut-1)
|
|
}
|
|
}
|