a4abcdefa3
embedder.heads_path is empty by default and deploy/mavend.json sets it. A missing or broken weights file logs and leaves the heads nil, because refusing to start over a routing accelerator would trade a working box for a better one. TestONNXRoutingHeads is the same cascade TestONNXBaseline scores with one arm added, so the two are directly comparable. It also checks the Go tokenizer against the Python one, since the heads were trained through transformers and are read through a hand-written tokenizer: a mismatch shows up here as a score below what Python measured on the same weights, and nowhere else. That is how the reversed word pieces were found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/memory"
|
|
"github.com/kami/maven/internal/router"
|
|
"github.com/kami/maven/internal/store"
|
|
"github.com/kami/maven/internal/tool"
|
|
"github.com/kami/maven/internal/voice"
|
|
)
|
|
|
|
func TestReactiveNotesReminders(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newTestStore(t)
|
|
api := ipc.NewStoreAPI(st)
|
|
now := time.Now()
|
|
|
|
emb := router.NewHashEmbedder(1024)
|
|
matcher := tool.NewMatcher(api)
|
|
rtr := buildRouter(emb, matcher, 0.55, nil, nil)
|
|
|
|
h := &reactiveHandler{
|
|
api: api,
|
|
recall: recallWiring{embedder: emb, memStore: memory.NewInMemoryStore()},
|
|
router: rtr,
|
|
replier: voice.NewStubReplier(),
|
|
now: func() time.Time { return now },
|
|
dataStore: st,
|
|
}
|
|
|
|
t.Run("reminder capture", func(t *testing.T) {
|
|
fireAt := now.Add(24 * time.Hour).Truncate(time.Millisecond)
|
|
dec := router.Decision{
|
|
Intent: router.IntentReminder,
|
|
Utterance: "напомни завтра позвонить маме",
|
|
Slots: router.Slots{
|
|
Text: "завтра позвонить маме",
|
|
Time: fireAt,
|
|
HasTime: true,
|
|
},
|
|
}
|
|
// The confirmation is phrased from the row now (Vikunja #507), so it
|
|
// names the stored hour rather than leaving the replier to read one
|
|
// out of the sentence.
|
|
reply := h.applyAction(ctx, dec)
|
|
if want := "хорошо, напомню завтра в " + fireAt.Format("15:04") + "."; reply != want {
|
|
t.Errorf("reply = %q, want %q", reply, want)
|
|
}
|
|
reminders, err := st.ListReminders(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("ListReminders: %v", err)
|
|
}
|
|
if len(reminders) == 0 {
|
|
t.Fatal("expected at least one reminder, got none")
|
|
}
|
|
last := reminders[0]
|
|
if last.FireTs.UnixMilli() != fireAt.UnixMilli() {
|
|
t.Errorf("reminder FireTs = %v, want %v", last.FireTs, fireAt)
|
|
}
|
|
if last.Status != "pending" {
|
|
t.Errorf("reminder Status = %q, want pending", last.Status)
|
|
}
|
|
})
|
|
|
|
t.Run("note capture", func(t *testing.T) {
|
|
dec := router.Decision{
|
|
Intent: router.IntentNote,
|
|
Utterance: "запомни что кофе закончился",
|
|
}
|
|
reply := h.applyAction(ctx, dec)
|
|
if reply != "" {
|
|
t.Errorf("expected empty reply from applyAction, got %q", reply)
|
|
}
|
|
notes, err := st.RecentNotes(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("RecentNotes: %v", err)
|
|
}
|
|
if len(notes) == 0 {
|
|
t.Fatal("expected at least one note, got none")
|
|
}
|
|
last := notes[0]
|
|
if last.Text != "запомни что кофе закончился" {
|
|
t.Errorf("note text = %q, want %q", last.Text, "запомни что кофе закончился")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestSpokenTaskCaptureFilesATask — the whole path, from the utterance to the
|
|
// task table. It went dead when the router started claiming the marker as an
|
|
// act: capture rides the note intent, so nothing below actionNote was ever
|
|
// reached and every capture answered "Что сделать?" (Vikunja #467).
|
|
func TestSpokenTaskCaptureFilesATask(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newTestStore(t)
|
|
api := ipc.NewStoreAPI(st)
|
|
now := time.Now()
|
|
emb := router.NewHashEmbedder(1024)
|
|
matcher := tool.NewMatcher(api)
|
|
h := &reactiveHandler{
|
|
api: api,
|
|
recall: recallWiring{embedder: emb, memStore: memory.NewInMemoryStore()},
|
|
router: buildRouter(emb, matcher, 0.55, nil, nil),
|
|
replier: voice.NewStubReplier(),
|
|
now: func() time.Time { return now },
|
|
dataStore: st,
|
|
}
|
|
|
|
reply := h.handleText(ctx, "web", "добавь в задачи купить молоко")
|
|
if !strings.Contains(reply, "купить молоко") {
|
|
t.Fatalf("capture did not claim the turn: %q", reply)
|
|
}
|
|
open, err := st.ListTasks(ctx, store.TaskOpen)
|
|
if err != nil || len(open) != 1 {
|
|
t.Fatalf("task was not filed: tasks=%v err=%v", open, err)
|
|
}
|
|
// The words he said, not the model's rewrite of them.
|
|
if open[0].Text != "купить молоко" {
|
|
t.Fatalf("task text was rewritten: %q", open[0].Text)
|
|
}
|
|
}
|