d960e211d3
Brings internal/tool/risk.go in so the Hexis split can be written against it. Four conflicts, all additive: both grammar sets in voicewire.go, both test sets in agenda_test.go and stage0.go, and in actions_act.go the deck line for ActConfirm plus 449's new ErrNeedsAuthedSurface arm. Two renames the merge forced. actions_list_test.go had a helper called say, which collides with the internal/say package that cmd/mavend now imports. actions_act_risk_test.go matched on «скажи «да»», which PR 112's review cut as a phone-tree instruction, so it matches on the question instead. --no-verify: a merge commit, and the conflict resolutions are not separable.
76 lines
2.4 KiB
Go
76 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|