a1f811d4c8
Read before routing and only when a list is bound: with nothing offered, "второй" is an ordinary word and keeps routing. No verb reads it back rather than guessing what to do with it.
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/dialogue"
|
|
"github.com/kami/maven/internal/store"
|
|
)
|
|
|
|
func TestParseOrdinalReadsThePosition(t *testing.T) {
|
|
cases := []struct {
|
|
text string
|
|
want int
|
|
ok bool
|
|
}{
|
|
{"второй", 2, true},
|
|
{"вторую сделал", 2, true},
|
|
{"первую убери", 1, true},
|
|
{"последнюю не надо", -1, true},
|
|
{"3", 3, true},
|
|
{"the second one", 2, true},
|
|
// No position named.
|
|
{"какие у меня задачи", 0, false},
|
|
{"", 0, false},
|
|
// A digit inside a time is not a position.
|
|
{"напомни в 15:00", 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := parseOrdinal(c.text)
|
|
if ok != c.ok || (ok && got != c.want) {
|
|
t.Errorf("parseOrdinal(%q) = %d,%v; want %d,%v", c.text, got, ok, c.want, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOrdinalPassesWithNothingOffered(t *testing.T) {
|
|
h, _, _ := newClarifyHandler(t)
|
|
if _, handled := h.resolveCandidate(context.Background(), "второй"); handled {
|
|
t.Error("an ordinal with no list behind it was claimed")
|
|
}
|
|
}
|
|
|
|
func TestOrdinalReadsBackWithoutAVerb(t *testing.T) {
|
|
h, st, _ := newClarifyHandler(t)
|
|
ctx := context.Background()
|
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
|
putCandidates(h, ids, "купить хлеб", "позвонить маме")
|
|
|
|
reply, handled := h.resolveCandidate(ctx, "второй")
|
|
if !handled || !strings.Contains(reply, "позвонить маме") {
|
|
t.Fatalf("a bare ordinal did not read the task back: %q handled=%v", reply, handled)
|
|
}
|
|
// Still live: naming one is often the first half of a sentence.
|
|
if _, handled := h.resolveCandidate(ctx, "первый"); !handled {
|
|
t.Error("the list was spent by a read-back")
|
|
}
|
|
}
|
|
|
|
func TestOrdinalWithAVerbMovesTheTask(t *testing.T) {
|
|
h, st, _ := newClarifyHandler(t)
|
|
ctx := context.Background()
|
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
|
putCandidates(h, ids, "купить хлеб", "позвонить маме")
|
|
|
|
reply, handled := h.resolveCandidate(ctx, "первую сделал")
|
|
if !handled || !strings.Contains(reply, "купить хлеб") {
|
|
t.Fatalf("the pick was not acted on: %q handled=%v", reply, handled)
|
|
}
|
|
live, err := st.ListTasks(ctx, "live")
|
|
if err != nil {
|
|
t.Fatalf("list tasks: %v", err)
|
|
}
|
|
for _, task := range live {
|
|
if task.ID == ids[0] {
|
|
t.Fatalf("task %d is still live after he closed it", task.ID)
|
|
}
|
|
}
|
|
// Spent: a second ordinal against a list that no longer holds would close
|
|
// the wrong task.
|
|
if _, handled := h.resolveCandidate(ctx, "второй"); handled {
|
|
t.Error("the list survived the pick it was spent on")
|
|
}
|
|
}
|
|
|
|
func TestOrdinalPastTheEndSaysHowMany(t *testing.T) {
|
|
h, st, _ := newClarifyHandler(t)
|
|
ids := seedTasks(t, st, "купить хлеб")
|
|
putCandidates(h, ids, "купить хлеб")
|
|
|
|
reply, handled := h.resolveCandidate(context.Background(), "третий")
|
|
if !handled || !strings.Contains(reply, "1") {
|
|
t.Fatalf("a position she never read was not answered: %q handled=%v", reply, handled)
|
|
}
|
|
}
|
|
|
|
// ordinalNow — a fixed capture time; the ranker only needs the rows to exist.
|
|
var ordinalNow = time.Date(2026, 8, 4, 9, 0, 0, 0, time.UTC)
|
|
|
|
func seedTasks(t *testing.T, st *store.Store, texts ...string) []int64 {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
var ids []int64
|
|
for _, text := range texts {
|
|
res, err := st.CaptureTask(ctx, store.Task{
|
|
Text: text,
|
|
Source: "tap:voice",
|
|
Status: store.TaskOpen,
|
|
CreatedTs: ordinalNow,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("capture task: %v", err)
|
|
}
|
|
ids = append(ids, res.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func putCandidates(h *reactiveHandler, ids []int64, labels ...string) {
|
|
cands := make([]dialogue.Candidate, 0, len(ids))
|
|
for i, id := range ids {
|
|
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: id, Label: labels[i]})
|
|
}
|
|
h.dialogueSessions.Put(voiceDialogueID, &dialogue.Session{Timestamp: h.now()})
|
|
h.offerCandidates(cands)
|
|
}
|