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) } }