1package modes
2
3import (
4 "errors"
5 "slices"
6 "strings"
7 "testing"
8
9 "github.com/patriceckhart/zot/packages/agent/skills"
10 "github.com/patriceckhart/zot/packages/tui"
11)
12
13func TestFuzzySkillSuggestions(t *testing.T) {
14 s := newSlashSuggester()
15 s.SetSkills([]*skills.Skill{
16 {Name: "code-review", DisableModelInvocation: true},
17 {Name: "review-code"},
18 {Name: "aaa", Description: "review"},
19 {Name: "review-builtin", Builtin: true},
20 })
21 check := func(query string, want ...string) {
22 t.Helper()
23 if got := commandNames(s.matches(query)); !slices.Equal(got, want) {
24 t.Fatalf("matches(%q) = %v, want %v", query, got, want)
25 }
26 }
27 check("/skill:review", "/skill:review-code")
28 check("/skill:crv")
29 s.SetFuzzySkills(true)
30 check("/skill:review", "/skill:review-code", "/skill:code-review")
31 check("/SKILL:CRV", "/skill:code-review")
32 check("/skill:", "/skill:aaa", "/skill:code-review", "/skill:review-code")
33 check("/skill:code-review", "/skill:code-review")
34 check("/skill:zzz")
35 check("/skill:skill") // The command prefix is not searched.
36 check("/skill:review request")
37 check("/x") // Other slash commands still require prefixes.
38 s.SetExtra([]slashCommand{{Name: "/code-review"}})
39 check("/review")
40 s.Render("/skill:review", tui.Theme{}, 80)
41 s.Down()
42 if got := s.Selection("/skill:review"); got != "/skill:code-review" {
43 t.Fatal(got)
44 }
45 s.SetFuzzySkills(false)
46 check("/skill:review", "/skill:review-code")
47 if s.cursor != 0 || s.lastMatches != nil {
48 t.Fatal("toggle did not reset selection")
49 }
50 for _, c := range s.matches("/skill:") {
51 if len(c.matchedIndexes) != 0 {
52 t.Fatal("highlight leaked into catalog")
53 }
54 }
55}
56
57func TestFuzzySkillSuggestionExactName(t *testing.T) {
58 s := newSlashSuggester()
59 s.SetSkills([]*skills.Skill{{Name: "review-more"}, {Name: "review"}, {Name: "code-review"}})
60 for _, enabled := range []bool{false, true} {
61 s.SetFuzzySkills(enabled)
62 if got := commandNames(s.matches("/SKILL:REVIEW")); !slices.Equal(got, []string{"/skill:review"}) {
63 t.Fatalf("exact match with fuzzy=%v: %v", enabled, got)
64 }
65 }
66 if _, recognized, err := expandSkillCommand("/skill:crv", []*skills.Skill{{Name: "code-review"}}); !recognized || err == nil {
67 t.Fatal("invocation must still require an exact name")
68 }
69}
70
71func TestFuzzySkillSuggestionTies(t *testing.T) {
72 s := newSlashSuggester()
73 s.SetSkills([]*skills.Skill{{Name: "bb-review"}, {Name: "aa-review"}})
74 s.SetFuzzySkills(true)
75 want := []string{"/skill:aa-review", "/skill:bb-review"}
76 for range 10 {
77 if got := commandNames(s.matches("/skill:review")); !slices.Equal(got, want) {
78 t.Fatalf("ties = %v", got)
79 }
80 }
81}
82
83func TestFuzzySkillSuggestionHighlight(t *testing.T) {
84 s := newSlashSuggester()
85 s.SetSkills([]*skills.Skill{{Name: "pré-review"}, {Name: "pré-revise"}})
86 s.SetFuzzySkills(true)
87 for _, width := range []int{1, 20, 80} {
88 lines := s.Render("/skill:Érv", tui.Theme{}, width)
89 for row := range 2 {
90 for _, char := range []string{"é", "r", "v"} {
91 if !strings.Contains(lines[row], tui.Bold(char)) {
92 t.Fatalf("missing emphasis for %q: %q", char, lines[row])
93 }
94 }
95 if !strings.Contains(lines[row], "/skill:") {
96 t.Fatalf("prefix was emphasized: %q", lines[row])
97 }
98 }
99 }
100}
101
102type fuzzySkillSettingsStore struct {
103 SettingsStore
104 value bool
105 err error
106}
107
108func (s *fuzzySkillSettingsStore) SetFuzzySkillSuggest(value bool) error {
109 if s.err != nil {
110 return s.err
111 }
112 s.value = value
113 return nil
114}
115
116func TestFuzzySkillSetting(t *testing.T) {
117 // Exercise the normal repaint path, independent of the developer's terminal.
118 t.Setenv("TERM_PROGRAM", "")
119 for _, enabled := range []bool{false, true} {
120 i := NewInteractive(InteractiveConfig{Terminal: &cleanupTestTerminal{}, FuzzySkillSuggest: &enabled})
121 if i.suggest.fuzzySkills != enabled {
122 t.Fatal("startup preference ignored")
123 }
124 }
125 store := &fuzzySkillSettingsStore{}
126 i := NewInteractive(InteractiveConfig{Terminal: &cleanupTestTerminal{}, SettingsStore: store})
127 if i.suggest.fuzzySkills {
128 t.Fatal("default must be off")
129 }
130 for _, value := range []bool{true, false} {
131 i.applySettingToggle("fuzzy_skill_suggest", value)
132 if store.value != value || i.suggest.fuzzySkills != value || i.cfg.FuzzySkillSuggest == nil || *i.cfg.FuzzySkillSuggest != value {
133 t.Fatal("toggle was not applied and persisted")
134 }
135 }
136 store.err = errors.New("save failed")
137 i.applySettingToggle("fuzzy_skill_suggest", true)
138 if i.suggest.fuzzySkills || *i.cfg.FuzzySkillSuggest || !strings.Contains(i.statusErr, "save failed") {
139 t.Fatal("failed save changed live preference or hid error")
140 }
141}