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.

💾 Saved. d722711 · on d72271127802973540c648bfb372176cdaaa8e4f · k33g · 6h ago
skills_test.go · 242 lines · 8.0 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Tests for the skills package. What matters here: the catalogue is a TOOL
// DESCRIPTION, so what the model knows about the available skills is exactly
// what these functions produce. One header read wrong, and the model can no
// longer name the skill it wants.
package skills

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// write creates a skill file in a temporary directory.
func write(t *testing.T, dir, name, content string) {
	t.Helper()
	if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
}

func TestParseHeader(t *testing.T) {
	cases := []struct {
		name     string
		content  string
		wantName string
		wantDesc string
	}{{
		name:     "complete header",
		content:  "---\nname: go-rename\ndescription: rename a symbol\n---\n# Title\n",
		wantName: "go-rename",
		wantDesc: "rename a symbol",
	}, {
		name:     "no header: the name comes from the file",
		content:  "# Just some markdown\n",
		wantName: "file",
		wantDesc: "",
	}, {
		name:     "empty name: the file name is kept",
		content:  "---\nname:\ndescription: something\n---\n",
		wantName: "file",
		wantDesc: "something",
	}, {
		// The case parseHeader's comment announces: a `description:` in the
		// BODY is not metadata.
		name:     "description in the body: ignored",
		content:  "---\nname: go-test\n---\ndescription: this is just text\n",
		wantName: "go-test",
		wantDesc: "",
	}, {
		name:     "spaces around the values",
		content:  "---\n  name  :   go-fmt  \n  description :  format code \n---\n",
		wantName: "go-fmt",
		wantDesc: "format code",
	}, {
		name:     "a colon inside the description",
		content:  "---\nname: go-run\ndescription: run: execute a program\n---\n",
		wantName: "go-run",
		wantDesc: "run: execute a program",
	}}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			s := parseHeader(c.content, "/tmp/file.md")
			if s.Name != c.wantName {
				t.Errorf("Name = %q, want %q", s.Name, c.wantName)
			}
			if s.Description != c.wantDesc {
				t.Errorf("Description = %q, want %q", s.Description, c.wantDesc)
			}
		})
	}
}

func TestListSortsAndReadsHeaders(t *testing.T) {
	dir := t.TempDir()
	write(t, dir, "zeta.md", "---\nname: zeta\ndescription: the last one\n---\n")
	write(t, dir, "alpha.md", "---\nname: alpha\ndescription: the first one\n---\n")
	write(t, dir, "notes.txt", "not a skill") // ignored: not *.md

	list := List(dir)
	if len(list) != 2 {
		t.Fatalf("%d skills, want 2: %+v", len(list), list)
	}
	if list[0].Name != "alpha" || list[1].Name != "zeta" {
		t.Errorf("expected a sort by name, got %q then %q", list[0].Name, list[1].Name)
	}
}

// A missing directory is not an error: this agent simply has no skills, and
// main.go will not declare the tool.
func TestListMissingDirIsEmpty(t *testing.T) {
	if list := List(filepath.Join(t.TempDir(), "doesnotexist")); len(list) != 0 {
		t.Errorf("%d skills for a missing directory, want 0", len(list))
	}
}

func TestRead(t *testing.T) {
	dir := t.TempDir()
	write(t, dir, "go-fmt.md", "---\nname: go-fmt\n---\n# Format\n")

	content, err := Read(dir, "go-fmt")
	if err != nil {
		t.Fatalf("Read: %v", err)
	}
	if !strings.Contains(content, "# Format") {
		t.Errorf("unexpected content: %q", content)
	}

	if _, err := Read(dir, "unknown"); err == nil {
		t.Error("an unknown skill must return an error")
	}
}

// filepath.Base in Read is a barrier: the name comes from the MODEL, so it must
// not be able to escape the skills directory.
func TestReadCannotEscapeDir(t *testing.T) {
	dir := t.TempDir()
	parent := filepath.Dir(dir)
	if err := os.WriteFile(filepath.Join(parent, "secret.md"), []byte("forbidden"), 0o644); err != nil {
		t.Fatal(err)
	}

	for _, name := range []string{"../secret", "../../secret", "/etc/passwd"} {
		if content, err := Read(dir, name); err == nil {
			t.Errorf("Read(%q) succeeded and returned %q — the barrier is gone", name, content)
		}
	}
}

// The catalogue IS the tool description: every skill must appear in it with its
// name, otherwise the model cannot ask for it.
func TestCatalogue(t *testing.T) {
	list := []Skill{
		{Name: "go-fmt", Description: "format code"},
		{Name: "go-test"}, // no description
	}
	cat := Catalogue(list)

	for _, want := range []string{"go-fmt", "format code", "go-test"} {
		if !strings.Contains(cat, want) {
			t.Errorf("the catalogue does not contain %q:\n%s", want, cat)
		}
	}
	if !strings.Contains(cat, "go-fmt — format code") {
		t.Errorf("name and description must be joined by a dash:\n%s", cat)
	}
	// A skill with no description must not leave an orphan dash.
	if strings.Contains(cat, "go-test —") {
		t.Errorf("orphan dash for a skill with no description:\n%s", cat)
	}
}

func TestNames(t *testing.T) {
	got := Names([]Skill{{Name: "a"}, {Name: "b"}})
	if strings.Join(got, ",") != "a,b" {
		t.Errorf("Names = %v", got)
	}
}

// The skills actually shipped in the repository's skills/ must all have a
// name AND a description: without a description, the model picks blindly.
//
// The path is the real layout — skills/ at the repository root, one directory
// per skill. A t.Skip() here would mean "I am testing nothing" without saying
// so, so a missing directory is an ERROR, not a skip.
func TestShippedSkillsHaveDescriptions(t *testing.T) {
	dir := filepath.Join("..", "..", "skills")
	list := List(dir)
	if len(list) == 0 {
		t.Fatalf("no skill found in %s — has the layout changed?", dir)
	}
	for _, s := range list {
		if s.Description == "" {
			t.Errorf("skill %q has no description", s.Name)
		}
		if strings.Contains(s.Name, " ") {
			t.Errorf("name %q contains a space: the model will not be able to quote it", s.Name)
		}
	}
	t.Logf("%d shipped skills, all described", len(list))
}

// writeNested stores a skill the Agent Skills way: <dir>/<name>/SKILL.md.
func writeNested(t *testing.T, dir, name, content string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, name, "SKILL.md"), []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
}

// The two layouts are both found, may be mixed, and sort together. A nested
// skill without a name in its front matter is named after its directory, not
// "SKILL"; a stray markdown file inside a skill's directory is not a skill.
func TestListAcceptsFlatAndNestedLayouts(t *testing.T) {
	dir := t.TempDir()
	write(t, dir, "zeta.md", "---\nname: zeta\ndescription: flat, named\n---\n")
	writeNested(t, dir, "alpha", "---\ndescription: nested, unnamed\n---\n# Alpha\n")
	writeNested(t, dir, "mid", "---\nname: renamed\ndescription: nested, named\n---\n")
	if err := os.WriteFile(filepath.Join(dir, "alpha", "notes.md"), []byte("not a skill"), 0o644); err != nil {
		t.Fatal(err)
	}

	list := List(dir)
	got := make([]string, 0, len(list))
	for _, s := range list {
		got = append(got, s.Name+"="+s.Description)
	}
	want := []string{"alpha=nested, unnamed", "renamed=nested, named", "zeta=flat, named"}
	if strings.Join(got, "|") != strings.Join(want, "|") {
		t.Errorf("List:\n got %v\nwant %v", got, want)
	}
}

// Read finds a skill whichever layout stores it, prefers the flat file when
// both exist (it is the more explicit of the two), and still cannot be walked
// out of the directory with a nested name: "../escape" is reduced to "escape",
// which does not exist inside dir.
func TestReadAcceptsFlatAndNestedLayouts(t *testing.T) {
	parent := t.TempDir()
	dir := filepath.Join(parent, "skills")
	writeNested(t, parent, "escape", "# outside")
	writeNested(t, dir, "nested-only", "# nested")
	write(t, dir, "both.md", "# flat wins")
	writeNested(t, dir, "both", "# nested loses")

	if got, err := Read(dir, "nested-only"); err != nil || got != "# nested" {
		t.Errorf("Read(nested-only) = %q, %v", got, err)
	}
	if got, err := Read(dir, "both"); err != nil || got != "# flat wins" {
		t.Errorf("Read(both) = %q, %v; want the flat file", got, err)
	}
	if _, err := Read(dir, "missing"); err == nil {
		t.Error("Read(missing) returned no error")
	}
	if _, err := Read(dir, "../escape"); err == nil {
		t.Error("Read(../escape) escaped the directory")
	}
}