package main import ( "context" "strings" "testing" "github.com/kami/maven/internal/router" ) // The act path speaks each tier (Vikunja #449): a safe row runs, a destructive // one costs a confirm turn, an irreversible one is refused with the reason. func TestActPathSpeaksTheTiers(t *testing.T) { h, st, _ := newClarifyHandler(t) ctx := context.Background() now := h.now() for _, tc := range []struct { name string cmd []string destructive bool }{ {"status", []string{"true"}, false}, {"restart", []string{"true"}, true}, {"wipe", []string{"rm", "-rf"}, true}, } { if _, err := st.ProposeTool(ctx, tc.name, "test", "homelab", now); err != nil { t.Fatalf("propose %s: %v", tc.name, err) } if err := st.EnableTool(ctx, tc.name, tc.cmd, tc.destructive, "homelab", now); err != nil { t.Fatalf("enable %s: %v", tc.name, err) } } act := func(fn string) string { return h.actionAct(ctx, router.Decision{ Intent: router.IntentAct, Utterance: fn, Slots: router.Slots{Fn: fn, HasFn: true}, }) } if reply := act("status"); !strings.HasPrefix(reply, "готово") { t.Errorf("safe act replied %q; want it to have run", reply) } // PR 112's review cut «скажи «да» или «нет».» — he knows how to answer a // yes/no question — so the confirm turn is recognised by the question. if reply := act("restart"); !strings.Contains(reply, "да или нет") { t.Errorf("destructive act replied %q; want a confirm turn", reply) } // Clear the confirm the destructive act parked, so what is pending after // the irreversible one is only what the irreversible one parked. h.mu.Lock() h.pending = nil h.mu.Unlock() reply := act("wipe") if strings.Contains(reply, "да или нет") { t.Fatalf("irreversible act asked for a confirm: %q", reply) } if !strings.Contains(reply, "не вернуть") { t.Errorf("irreversible act replied %q; want it to name the reason", reply) } // Nothing was parked, so a later "да" cannot pick it up. h.mu.Lock() pending := h.pending h.mu.Unlock() if pending != nil { t.Errorf("an irreversible act parked %+v", pending) } // And it is still an enabled row — refusing to run it from voice is not // the same as taking it off the allowlist. if got, err := st.LookupTool(ctx, "wipe"); err != nil || got.Status != "enabled" { t.Errorf("wipe is %+v, %v; want it still enabled", got, err) } }