package router import ( "context" "testing" ) // agendaRouter wires both grammar sets in the order the daemon wires them // (voicewire.go): the clock rules first, the agenda rules after, so a test // that passes here is a test of the deployed precedence. func agendaRouter(t *testing.T) *Router { t.Helper() r := newTestRouter(t, 0.0) r.grammars = append(r.grammars, SystemTimeDateGrammars()...) r.grammars = append(r.grammars, AgendaQueryGrammars()...) return r } // An agenda question is answered from the calendar, which lives in the query // chain. Routed to system it reaches replySystem, which has no agenda arm and // says "пока не умею" — seen on the deployed daemon, 01-08-2026. func TestAgendaQuestionsRouteToQuery(t *testing.T) { r := agendaRouter(t) for _, u := range []string{ "что у меня сегодня", "что у меня в календаре сегодня", "что у меня стоит в календаре на послезавтра", "какие у меня встречи завтра", "покажи расписание на среду", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("route(%q): %v", u, err) } if d.Intent != IntentQuery { t.Errorf("route(%q) = %s, want query", u, d.Intent) } } } // The clock rules keep their utterances. They are registered first and the // agenda patterns do not match them, so both statements have to hold. func TestAgendaGrammarsLeaveTheClockAlone(t *testing.T) { r := agendaRouter(t) for _, u := range []string{ "какой сегодня день", "какое сегодня число", "который час", "сколько сейчас времени", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("route(%q): %v", u, err) } if d.Intent != IntentSystem { t.Errorf("route(%q) = %s, want system", u, d.Intent) } } } // The agenda pattern is anchored and needs the possessive, so an ordinary // statement that happens to contain "у меня" is not swallowed. func TestAgendaGrammarSparesStatements(t *testing.T) { r := agendaRouter(t) for _, u := range []string{ "у меня кончилась вода", "напомни мне завтра позвонить маме", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("route(%q): %v", u, err) } if d.Stage == 0 && d.Intent == IntentQuery { t.Errorf("route(%q) was claimed by the agenda grammar", u) } } } // The two shapes that carried no question mark and no interrogative, so the // model saw them first and called them facts (Vikunja #498). func TestNarrativeGrammarsRouteToQuery(t *testing.T) { r := agendaRouter(t) r.grammars = append(r.grammars, NarrativeQueryGrammars()...) for _, u := range []string{ "что дальше?", "и что там дальше", "what's next?", "расскажи про битву при Ватерлоо", "объясни как работает дизель", "опиши Ватерлоо", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("%q: %v", u, err) } if d.Intent != IntentQuery || d.Stage != 0 { t.Errorf("%q routed intent=%s stage=%d, want query at stage 0", u, d.Intent, d.Stage) } } } // The tomorrow form and the bare event noun. Both were measured answering // "пока не умею" on the deployed daemon, 02-08-2026, while the same question // about today worked — the first rule set needed "у меня" or a calendar noun // and these phrasings carry neither (Vikunja #471). func TestAgendaCoversOtherDaysAndNamedEvents(t *testing.T) { r := agendaRouter(t) for _, u := range []string{ "какие планы на завтра?", "какие планы на послезавтра", "что по делам в среду", "какие планы на выходные", "когда планёрка?", "во сколько созвон", "когда будет совещание", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("route(%q): %v", u, err) } if d.Intent != IntentQuery { t.Errorf("route(%q) = %s, want query", u, d.Intent) } } } // A narrative verb next to a capture verb is him asking for a note. Stage 0 // declines and the extractor gets its turn. func TestNarrativeGrammarLeavesCapturesAlone(t *testing.T) { r := agendaRouter(t) r.grammars = append(r.grammars, NarrativeQueryGrammars()...) d, err := r.Route(context.Background(), "расскажи и запиши что я пил воду", refNow()) if err != nil { t.Fatal(err) } if d.Stage == 0 && d.Intent == IntentQuery { t.Errorf("stage 0 claimed a capture: %+v", d) } } // The two new rules are narrow on purpose. A world question that opens with // "когда" is not an agenda question, and telling her about a plan is not // asking about one. func TestAgendaGrammarsLeaveTheWorldAlone(t *testing.T) { r := agendaRouter(t) for _, u := range []string{ "когда была битва при ватерлоо", "когда изобрели телефон", } { d, err := r.Route(context.Background(), u, refNow()) if err != nil { t.Fatalf("route(%q): %v", u, err) } if d.Stage == 0 { t.Errorf("route(%q) was claimed at stage 0 as %s", u, d.Intent) } } }