Files
Maven/internal/router/praxis_test.go
T
claude a97764c5f7 Add seven stage 0 frames and tighten three more (V-720)
MavenHelpGrammar keeps "как отменить напоминание" on SourceSelf, where the
answer names the command Maven accepts, instead of leaking to search.
PublicCurrentVersionGrammar anchors an explicitly current release on
SourceWorld and declines first-person ownership.

AmbiguousFragmentGrammar refuses filler plus an unresolved demonstrative
rather than letting a statistical head invent context.
ImplicitElapsedQueryGrammar reads Russian question word order in "давно я
не тренировался" as recall; the declarative order stays a statement.
ReminderCancellationReportGrammar keeps "я отменил напоминание" in the
non-mutating chat lane.

CommandProhibitionGrammar routes a direct negative command to a sentinel
fn that can never collide with an enabled tool. ActHasEntityTarget stops a
bare verb or a demonstrative-only tail from crossing into Nexus.

Praxis attention now accepts "что там с X" for the four service names only.
taskstatus separates command mood from result words so a first-person
report cannot mutate the board. question.go exports the open-question and
locative shapes the recall gate reads.

--no-verify: master is the working branch this session by the owner's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:18:48 +04:00

225 lines
8.3 KiB
Go

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 TestPraxisServiceAttentionClaimsOnlyArchitectureServices(t *testing.T) {
g := grammarByName(t, "praxis-service-attention")
for _, utterance := range []string{
"что там с нексусом?",
"что там с праксисом",
"что там с хексисом!",
"что там с мавеном",
} {
decision, ok := matchGrammar(g, utterance)
if !ok || decision.Slots.Fn != "entity_attention" || decision.Slots.Text == "" {
t.Errorf("%q = %+v, ok=%v; want scoped attention", utterance, decision, ok)
}
}
for _, utterance := range []string{
"что там с погодой?",
"что там с бэкапами?",
"что там с сервером?",
} {
if decision, ok := matchGrammar(g, utterance); ok {
t.Errorf("%q was claimed as %+v", utterance, decision)
}
}
}
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) {
decision, _, accepted := g.Evaluate(utt)
return decision, accepted
}