bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

skills_test.go · 242 lines · 8.0 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 7h ago1// Tests for the skills package. What matters here: the catalogue is a TOOL
2// DESCRIPTION, so what the model knows about the available skills is exactly
3// what these functions produce. One header read wrong, and the model can no
4// longer name the skill it wants.
5package skills
6
7import (
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12)
13
14// write creates a skill file in a temporary directory.
15func write(t *testing.T, dir, name, content string) {
16 t.Helper()
17 if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
18 t.Fatal(err)
19 }
20}
21
22func TestParseHeader(t *testing.T) {
23 cases := []struct {
24 name string
25 content string
26 wantName string
27 wantDesc string
28 }{{
29 name: "complete header",
30 content: "---\nname: go-rename\ndescription: rename a symbol\n---\n# Title\n",
31 wantName: "go-rename",
32 wantDesc: "rename a symbol",
33 }, {
34 name: "no header: the name comes from the file",
35 content: "# Just some markdown\n",
36 wantName: "file",
37 wantDesc: "",
38 }, {
39 name: "empty name: the file name is kept",
40 content: "---\nname:\ndescription: something\n---\n",
41 wantName: "file",
42 wantDesc: "something",
43 }, {
44 // The case parseHeader's comment announces: a `description:` in the
45 // BODY is not metadata.
46 name: "description in the body: ignored",
47 content: "---\nname: go-test\n---\ndescription: this is just text\n",
48 wantName: "go-test",
49 wantDesc: "",
50 }, {
51 name: "spaces around the values",
52 content: "---\n name : go-fmt \n description : format code \n---\n",
53 wantName: "go-fmt",
54 wantDesc: "format code",
55 }, {
56 name: "a colon inside the description",
57 content: "---\nname: go-run\ndescription: run: execute a program\n---\n",
58 wantName: "go-run",
59 wantDesc: "run: execute a program",
60 }}
61
62 for _, c := range cases {
63 t.Run(c.name, func(t *testing.T) {
64 s := parseHeader(c.content, "/tmp/file.md")
65 if s.Name != c.wantName {
66 t.Errorf("Name = %q, want %q", s.Name, c.wantName)
67 }
68 if s.Description != c.wantDesc {
69 t.Errorf("Description = %q, want %q", s.Description, c.wantDesc)
70 }
71 })
72 }
73}
74
75func TestListSortsAndReadsHeaders(t *testing.T) {
76 dir := t.TempDir()
77 write(t, dir, "zeta.md", "---\nname: zeta\ndescription: the last one\n---\n")
78 write(t, dir, "alpha.md", "---\nname: alpha\ndescription: the first one\n---\n")
79 write(t, dir, "notes.txt", "not a skill") // ignored: not *.md
80
81 list := List(dir)
82 if len(list) != 2 {
83 t.Fatalf("%d skills, want 2: %+v", len(list), list)
84 }
85 if list[0].Name != "alpha" || list[1].Name != "zeta" {
86 t.Errorf("expected a sort by name, got %q then %q", list[0].Name, list[1].Name)
87 }
88}
89
90// A missing directory is not an error: this agent simply has no skills, and
91// main.go will not declare the tool.
92func TestListMissingDirIsEmpty(t *testing.T) {
93 if list := List(filepath.Join(t.TempDir(), "doesnotexist")); len(list) != 0 {
94 t.Errorf("%d skills for a missing directory, want 0", len(list))
95 }
96}
97
98func TestRead(t *testing.T) {
99 dir := t.TempDir()
100 write(t, dir, "go-fmt.md", "---\nname: go-fmt\n---\n# Format\n")
101
102 content, err := Read(dir, "go-fmt")
103 if err != nil {
104 t.Fatalf("Read: %v", err)
105 }
106 if !strings.Contains(content, "# Format") {
107 t.Errorf("unexpected content: %q", content)
108 }
109
110 if _, err := Read(dir, "unknown"); err == nil {
111 t.Error("an unknown skill must return an error")
112 }
113}
114
115// filepath.Base in Read is a barrier: the name comes from the MODEL, so it must
116// not be able to escape the skills directory.
117func TestReadCannotEscapeDir(t *testing.T) {
118 dir := t.TempDir()
119 parent := filepath.Dir(dir)
120 if err := os.WriteFile(filepath.Join(parent, "secret.md"), []byte("forbidden"), 0o644); err != nil {
121 t.Fatal(err)
122 }
123
124 for _, name := range []string{"../secret", "../../secret", "/etc/passwd"} {
125 if content, err := Read(dir, name); err == nil {
126 t.Errorf("Read(%q) succeeded and returned %q — the barrier is gone", name, content)
127 }
128 }
129}
130
131// The catalogue IS the tool description: every skill must appear in it with its
132// name, otherwise the model cannot ask for it.
133func TestCatalogue(t *testing.T) {
134 list := []Skill{
135 {Name: "go-fmt", Description: "format code"},
136 {Name: "go-test"}, // no description
137 }
138 cat := Catalogue(list)
139
140 for _, want := range []string{"go-fmt", "format code", "go-test"} {
141 if !strings.Contains(cat, want) {
142 t.Errorf("the catalogue does not contain %q:\n%s", want, cat)
143 }
144 }
145 if !strings.Contains(cat, "go-fmt — format code") {
146 t.Errorf("name and description must be joined by a dash:\n%s", cat)
147 }
148 // A skill with no description must not leave an orphan dash.
149 if strings.Contains(cat, "go-test —") {
150 t.Errorf("orphan dash for a skill with no description:\n%s", cat)
151 }
152}
153
154func TestNames(t *testing.T) {
155 got := Names([]Skill{{Name: "a"}, {Name: "b"}})
156 if strings.Join(got, ",") != "a,b" {
157 t.Errorf("Names = %v", got)
158 }
159}
160
161// The skills actually shipped in the repository's skills/ must all have a
162// name AND a description: without a description, the model picks blindly.
163//
164// The path is the real layout — skills/ at the repository root, one directory
165// per skill. A t.Skip() here would mean "I am testing nothing" without saying
166// so, so a missing directory is an ERROR, not a skip.
167func TestShippedSkillsHaveDescriptions(t *testing.T) {
168 dir := filepath.Join("..", "..", "skills")
169 list := List(dir)
170 if len(list) == 0 {
171 t.Fatalf("no skill found in %s — has the layout changed?", dir)
172 }
173 for _, s := range list {
174 if s.Description == "" {
175 t.Errorf("skill %q has no description", s.Name)
176 }
177 if strings.Contains(s.Name, " ") {
178 t.Errorf("name %q contains a space: the model will not be able to quote it", s.Name)
179 }
180 }
181 t.Logf("%d shipped skills, all described", len(list))
182}
183
184// writeNested stores a skill the Agent Skills way: <dir>/<name>/SKILL.md.
185func writeNested(t *testing.T, dir, name, content string) {
186 t.Helper()
187 if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil {
188 t.Fatal(err)
189 }
190 if err := os.WriteFile(filepath.Join(dir, name, "SKILL.md"), []byte(content), 0o644); err != nil {
191 t.Fatal(err)
192 }
193}
194
195// The two layouts are both found, may be mixed, and sort together. A nested
196// skill without a name in its front matter is named after its directory, not
197// "SKILL"; a stray markdown file inside a skill's directory is not a skill.
198func TestListAcceptsFlatAndNestedLayouts(t *testing.T) {
199 dir := t.TempDir()
200 write(t, dir, "zeta.md", "---\nname: zeta\ndescription: flat, named\n---\n")
201 writeNested(t, dir, "alpha", "---\ndescription: nested, unnamed\n---\n# Alpha\n")
202 writeNested(t, dir, "mid", "---\nname: renamed\ndescription: nested, named\n---\n")
203 if err := os.WriteFile(filepath.Join(dir, "alpha", "notes.md"), []byte("not a skill"), 0o644); err != nil {
204 t.Fatal(err)
205 }
206
207 list := List(dir)
208 got := make([]string, 0, len(list))
209 for _, s := range list {
210 got = append(got, s.Name+"="+s.Description)
211 }
212 want := []string{"alpha=nested, unnamed", "renamed=nested, named", "zeta=flat, named"}
213 if strings.Join(got, "|") != strings.Join(want, "|") {
214 t.Errorf("List:\n got %v\nwant %v", got, want)
215 }
216}
217
218// Read finds a skill whichever layout stores it, prefers the flat file when
219// both exist (it is the more explicit of the two), and still cannot be walked
220// out of the directory with a nested name: "../escape" is reduced to "escape",
221// which does not exist inside dir.
222func TestReadAcceptsFlatAndNestedLayouts(t *testing.T) {
223 parent := t.TempDir()
224 dir := filepath.Join(parent, "skills")
225 writeNested(t, parent, "escape", "# outside")
226 writeNested(t, dir, "nested-only", "# nested")
227 write(t, dir, "both.md", "# flat wins")
228 writeNested(t, dir, "both", "# nested loses")
229
230 if got, err := Read(dir, "nested-only"); err != nil || got != "# nested" {
231 t.Errorf("Read(nested-only) = %q, %v", got, err)
232 }
233 if got, err := Read(dir, "both"); err != nil || got != "# flat wins" {
234 t.Errorf("Read(both) = %q, %v; want the flat file", got, err)
235 }
236 if _, err := Read(dir, "missing"); err == nil {
237 t.Error("Read(missing) returned no error")
238 }
239 if _, err := Read(dir, "../escape"); err == nil {
240 t.Error("Read(../escape) escaped the directory")
241 }
242}