package router import "testing" // A lifecycle word alone is ordinary speech. Praxis mutations need an item named // too, and the two agreement words mean different states (Vikunja #516). func TestPraxisLifecycleNeedsAnItem(t *testing.T) { for _, utt := range []string{ "готово", "принято", "сделано, спасибо", "понял", "неважно", "я всё сделал", } { if c, ok := ParsePraxisLifecycle(utt); ok { t.Errorf("%q claimed as %+v; a bare lifecycle word must not transition an item", utt, c) } } } func TestPraxisLifecycleVerbPicksTheCapability(t *testing.T) { cases := []struct { utt, fn, ref string }{ {"отметь второй пункт", "acknowledge_item", "2"}, {"принято по первому пункту", "acknowledge_item", "1"}, {"пункт три готово", "resolve_item", "3"}, {"закрой последний пункт", "resolve_item", "last"}, {"игнорируй второй пункт", "ignore_item", "2"}, {"закрепи третий пункт", "pin_item", "3"}, {"resolve item_ab12", "resolve_item", "item_ab12"}, // The noun with no position: the capability asks which one, which is // better than guessing and better than falling to the model. {"отметь пункт", "acknowledge_item", ""}, } for _, c := range cases { got, ok := ParsePraxisLifecycle(c.utt) if !ok { t.Errorf("%q was not claimed", c.utt) continue } if got.Fn != c.fn || got.Ref != c.ref { t.Errorf("%q = %+v, want fn=%s ref=%s", c.utt, got, c.fn, c.ref) } } } // A demonstrative stands in for the item noun, because "отметь это как // сделанное" is what he says to a digest she just read. Which item it is, is the // daemon's question — the router only says that he pointed at one. func TestPraxisLifecycleAcceptsADemonstrative(t *testing.T) { for _, c := range []struct{ utt, fn string }{ {"отметь это как сделанное", "resolve_item"}, {"принято, я это видел", "acknowledge_item"}, {"игнорировать это пока", "ignore_item"}, {"закрепи это", "pin_item"}, } { got, ok := ParsePraxisLifecycle(c.utt) if !ok { t.Errorf("%q was not claimed", c.utt) continue } if got.Fn != c.fn || got.Ref != "this" { t.Errorf("%q = %+v, want fn=%s ref=this", c.utt, got, c.fn) } } } func TestPraxisAttentionGrammar(t *testing.T) { g := grammarByName(t, "praxis-attention") for _, utt := range []string{"что требует внимания", "что сейчас требует внимания?", "что нового по проектам"} { d, ok := matchGrammar(g, utt) if !ok { t.Errorf("%q was not claimed", utt) continue } if d.Slots.Fn != "list_attention" { t.Errorf("%q = fn %q", utt, d.Slots.Fn) } } // The feeds own the unqualified form. for _, utt := range []string{"что нового", "что нового в мире"} { if _, ok := matchGrammar(g, utt); ok { t.Errorf("%q belongs to the feeds, not Praxis", utt) } } } // An imperative is addressed to her, so it claims the turn with an empty slot and // the capability asks which пункт. A stative word in the same position does not. func TestImperativeClaimsAndAsksButStativeDoesNot(t *testing.T) { for _, c := range []struct{ utt, fn string }{ {"закрывай", "resolve_item"}, {"игнорируй пока", "ignore_item"}, {"закрепи", "pin_item"}, } { got, ok := ParsePraxisLifecycle(c.utt) if !ok || got.Fn != c.fn || got.Ref != "" { t.Errorf("%q = %+v, %v; want fn=%s with an empty ref", c.utt, got, ok, c.fn) } } // An imperative with an object of its own is about that object: "закрой // шторы" closes the curtains through Hexis and is not a Praxis turn. for _, utt := range []string{"закрой шторы в комнате", "закрой дверь", "пропусти песню"} { if got, ok := ParsePraxisLifecycle(utt); ok { t.Errorf("%q claimed as %+v; it names its own object", utt, got) } } for _, utt := range []string{"готово", "принято", "неважно", "решено"} { if got, ok := ParsePraxisLifecycle(utt); ok { t.Errorf("%q claimed as %+v; a stative word needs an item named", utt, got) } } } // "отметь" records a state and names none, so it cannot pick the transition by // itself: with a state in the sentence the state wins, without one it is the // weaker of the two. func TestMarkerVerbTakesTheStateFromTheSentence(t *testing.T) { if got, _ := ParsePraxisLifecycle("отметь второй пункт как сделанный"); got.Fn != "resolve_item" { t.Errorf("a named state should win, got %+v", got) } if got, _ := ParsePraxisLifecycle("отметь второй пункт"); got.Fn != "acknowledge_item" { t.Errorf("a marker with no state should acknowledge, got %+v", got) } // The marker is also a capture verb, so it must not claim a note. if got, ok := ParsePraxisLifecycle("отметь что молоко закончилось"); ok { t.Errorf("a note was claimed as a Praxis turn: %+v", got) } } // Two transitions in one sentence are not one transition. func TestPraxisLifecycleRefusesTwoVerbs(t *testing.T) { if c, ok := ParsePraxisLifecycle("первый пункт принято, готово"); ok { t.Errorf("two lifecycle verbs resolved to %+v instead of declining", c) } } // "готов" must not fire inside "готовлю": tokens, not substrings. func TestPraxisLifecycleDoesNotMatchInsideAWord(t *testing.T) { if _, ok := ParsePraxisLifecycle("готовлю первый пункт меню"); ok { t.Error("готовлю matched the resolve verb готов") } } func TestPraxisChangesGrammar(t *testing.T) { g := grammarByName(t, "praxis-changes") for _, utt := range []string{"что изменилось?", "какие изменения?", "покажи изменения", "what changed?"} { if _, ok := matchGrammar(g, utt); !ok { t.Errorf("%q was not claimed by praxis-changes", utt) } } // The feeds source owns "что нового", and two paths to one answer disagree. for _, utt := range []string{"что нового?", "что нового в мире?", "изменения погоды не волнуют"} { if _, ok := matchGrammar(g, utt); ok { t.Errorf("%q should not be a Praxis changes turn", utt) } } } func TestPraxisEntityAttentionNeedsASubject(t *testing.T) { g := grammarByName(t, "praxis-entity-attention") for _, c := range []struct{ utt, subject string }{ {"статус мавена", "мавена"}, {"как дела у праксиса?", "праксиса"}, {"status of hexis", "of hexis"}, } { d, ok := matchGrammar(g, c.utt) if !ok { t.Errorf("%q was not claimed", c.utt) continue } if d.Slots.Fn != "entity_attention" || d.Slots.Text != c.subject { t.Errorf("%q = fn %q text %q, want entity_attention / %q", c.utt, d.Slots.Fn, d.Slots.Text, c.subject) } } // A greeting has no subject and must not reach Nexus. for _, utt := range []string{"как дела?", "как дела у тебя", "статус", "привет"} { if _, ok := matchGrammar(g, utt); ok && utt != "как дела у тебя" { t.Errorf("%q claimed as scoped attention", utt) } } } func grammarByName(t *testing.T, name string) Grammar { t.Helper() for _, g := range PraxisGrammars() { if g.Name == name { return g } } t.Fatalf("no grammar named %q", name) return Grammar{} } func matchGrammar(g Grammar, utt string) (Decision, bool) { m := g.Pattern.FindStringSubmatch(utt) if m == nil { return Decision{}, false } return g.Build(m) }