v0.29.1 d92a60f

skills_test.go RawHistoryFolder

1package skills
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7)
8
9func TestSplitFrontmatter(t *testing.T) {
10	in := "---\nname: foo\ndescription: bar\n---\nbody text\n"
11	front, body := splitFrontmatter(in)
12	if front != "name: foo\ndescription: bar" {
13		t.Errorf("front = %q", front)
14	}
15	if body != "body text\n" {
16		t.Errorf("body = %q", body)
17	}
18
19	front2, body2 := splitFrontmatter("no frontmatter here")
20	if front2 != "" || body2 != "no frontmatter here" {
21		t.Errorf("expected pass-through, got front=%q body=%q", front2, body2)
22	}
23}
24
25func TestParseFrontmatter(t *testing.T) {
26	front := `name: code-review
27description: "Review a recent change."
28disable-model-invocation: true
29allowed-tools: [read, bash]
30permissions:
31  bash: ["git diff*", "git log*"]
32`
33	s := &Skill{}
34	parseFrontmatter(front, s)
35	if s.Name != "code-review" {
36		t.Errorf("name = %q", s.Name)
37	}
38	if s.Description != "Review a recent change." {
39		t.Errorf("description = %q", s.Description)
40	}
41	if !s.DisableModelInvocation {
42		t.Error("disable-model-invocation was not parsed")
43	}
44	if got := s.AllowedTools; len(got) != 2 || got[0] != "read" || got[1] != "bash" {
45		t.Errorf("allowed-tools = %v", got)
46	}
47	if got := s.Permissions["bash"]; len(got) != 2 || got[0] != "git diff*" || got[1] != "git log*" {
48		t.Errorf("permissions[bash] = %v", got)
49	}
50}
51
52func TestDiscoverProjectAndGlobalPriorityAndDedup(t *testing.T) {
53	t.Setenv("ZUT_AGENT_SKILLS", "")
54	tmp := t.TempDir()
55	zutHome := filepath.Join(tmp, "home")
56	cwd := filepath.Join(tmp, "proj")
57
58	mk := func(dir, name, desc string) {
59		full := filepath.Join(dir, name)
60		os.MkdirAll(full, 0o755)
61		body := "---\nname: " + name + "\ndescription: " + desc + "\n---\n# " + name + "\n"
62		os.WriteFile(filepath.Join(full, "SKILL.md"), []byte(body), 0o644)
63	}
64
65	// Same skill name in BOTH project and global; project should win.
66	mk(filepath.Join(cwd, ".zut", "skills"), "shared", "project version")
67	mk(filepath.Join(zutHome, "skills"), "shared", "global version")
68	// Unique skill in global only.
69	mk(filepath.Join(zutHome, "skills"), "global-only", "from global")
70
71	skills, errs := Discover(zutHome, cwd, "", true /* includeUser */)
72	if len(errs) > 0 {
73		t.Fatalf("errs: %v", errs)
74	}
75	// Expect the two user skills + every built-in shipped with the
76	// binary (currently the write-zut-extension authoring guide).
77	builtins := loadBuiltins()
78	want := 2 + len(builtins)
79	if len(skills) != want {
80		t.Fatalf("expected %d skills (2 user + %d built-in), got %d (%v)", want, len(builtins), len(skills), skills)
81	}
82	shared := FindByName(skills, "shared")
83	if shared == nil || shared.Description != "project version" {
84		t.Errorf("expected project to win for 'shared', got %v", shared)
85	}
86	if FindByName(skills, "global-only") == nil {
87		t.Errorf("global-only skill missing")
88	}
89	// At least one built-in should have made it through.
90	for _, b := range builtins {
91		if FindByName(skills, b.Name) == nil {
92			t.Errorf("built-in skill %q missing from Discover output", b.Name)
93		}
94	}
95}
96
97func TestDiscoverWithBundledSkillsPrecedence(t *testing.T) {
98	t.Setenv("ZUT_AGENT_SKILLS", "")
99	tmp := t.TempDir()
100	cwd := filepath.Join(tmp, "project")
101	userDir := filepath.Join(cwd, ".zut", "skills")
102	bundleA := filepath.Join(tmp, "ext-a", "skills")
103	bundleB := filepath.Join(tmp, "ext-b", "skills")
104	write := func(root, name, description string) {
105		dir := filepath.Join(root, name)
106		if err := os.MkdirAll(dir, 0o755); err != nil {
107			t.Fatal(err)
108		}
109		body := "---\nname: " + name + "\ndescription: " + description + "\n---\nbody\n"
110		if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(body), 0o644); err != nil {
111			t.Fatal(err)
112		}
113	}
114	write(userDir, "shared", "user")
115	write(bundleA, "shared", "bundle-a")
116	write(bundleA, "only-a", "only a")
117	write(bundleB, "only-a", "bundle-b")
118	write(bundleB, "only-b", "only b")
119
120	list, errs := DiscoverWithBundled("", cwd, "", true, []BundledSkillDir{
121		{Dir: bundleA, Source: "extension a"},
122		{Dir: bundleB, Source: "extension b"},
123	})
124	if len(errs) > 0 {
125		t.Fatalf("errs: %v", errs)
126	}
127	if got := FindByName(list, "shared"); got == nil || got.Description != "user" {
128		t.Fatalf("user precedence = %#v", got)
129	}
130	if got := FindByName(list, "only-a"); got == nil || got.Description != "only a" || got.Source != "extension a" {
131		t.Fatalf("bundle declaration precedence = %#v", got)
132	}
133	if got := FindByName(list, "only-b"); got == nil || got.Source != "extension b" {
134		t.Fatalf("second bundle skill = %#v", got)
135	}
136	builtins := loadBuiltins()
137	if len(builtins) == 0 {
138		t.Fatal("expected at least one builtin skill")
139	}
140	write(bundleA, builtins[0].Name, "bundled override")
141	list, errs = DiscoverWithBundled("", cwd, "", true, []BundledSkillDir{{Dir: bundleA, Source: "extension a"}})
142	if len(errs) > 0 {
143		t.Fatalf("builtin override errors: %v", errs)
144	}
145	if got := FindByName(list, builtins[0].Name); got == nil || got.Description != "bundled override" || got.Builtin {
146		t.Fatalf("bundled builtin override = %#v", got)
147	}
148}
149
150func TestVisibleSkillsHidesBuiltins(t *testing.T) {
151	in := []*Skill{
152		{Name: "user-one"},
153		{Name: "built-one", Builtin: true},
154		{Name: "user-two"},
155	}
156	out := VisibleSkills(in)
157	if len(out) != 2 {
158		t.Fatalf("expected 2 visible skills, got %d (%v)", len(out), out)
159	}
160	for _, s := range out {
161		if s.Builtin {
162			t.Errorf("built-in %q leaked into visible set", s.Name)
163		}
164	}
165}
166
167func TestDiscoverRecursesNestedAgentSkillsAndSupportsPathAlias(t *testing.T) {
168	t.Setenv("ZUT_AGENT_SKILLS", "")
169	tmp := t.TempDir()
170	home := filepath.Join(tmp, "home")
171	cwd := filepath.Join(tmp, "project")
172	root := filepath.Join(home, ".agents", "skills")
173	parentDir := filepath.Join(root, "systems-backend")
174	nestedDir := filepath.Join(parentDir, "subskills", "golang-patterns")
175	if err := os.MkdirAll(nestedDir, 0o755); err != nil {
176		t.Fatal(err)
177	}
178	if err := os.WriteFile(filepath.Join(parentDir, "SKILL.md"), []byte("---\nname: systems-backend\ndescription: router\n---\nrouter body\n"), 0o644); err != nil {
179		t.Fatal(err)
180	}
181	if err := os.WriteFile(filepath.Join(nestedDir, "SKILL.md"), []byte("---\nname: golang-patterns\ndescription: Go patterns\ndisable-model-invocation: true\n---\nGo body\n"), 0o644); err != nil {
182		t.Fatal(err)
183	}
184
185	list, errs := Discover("", cwd, home, true)
186	if len(errs) != 0 {
187		t.Fatalf("Discover() errors = %v", errs)
188	}
189	if got := FindByName(list, "systems-backend"); got == nil || got.Source != "global (agents)" {
190		t.Fatalf("parent skill = %#v", got)
191	}
192	nested := FindByName(list, "golang-patterns")
193	if nested == nil {
194		t.Fatalf("nested skill was not discovered: %#v", list)
195	}
196	if nested.Source != "global (agents)" || nested.Path != filepath.Join(nestedDir, "SKILL.md") {
197		t.Fatalf("nested skill metadata = %#v", nested)
198	}
199	if nested.Body != "Go body" {
200		t.Fatalf("nested skill body = %q", nested.Body)
201	}
202	alias := "systems-backend/subskills/golang-patterns"
203	if got := FindByName(list, alias); got != nested {
204		t.Fatalf("FindByName(%q) = %#v, want nested skill %#v", alias, got, nested)
205	}
206}
207
208func TestSystemPromptAddendum(t *testing.T) {
209	skills := []*Skill{
210		{Name: "built-a", Description: "Do A.", Builtin: true},
211		{Name: "user-b", Description: "Do B.", Path: "/tmp/skills/user-b/SKILL.md"},
212		{Name: "manual-c", Description: "Do C.", DisableModelInvocation: true},
213	}
214	out := SystemPromptAddendum(skills)
215	if !contains(out, "- built-a [builtin]: Do A.\n") {
216		t.Errorf("builtin entry missing or wrong:\n%s", out)
217	}
218	if !contains(out, "- user-b [/tmp/skills/user-b/SKILL.md]: Do B.\n") {
219		t.Errorf("user entry missing path pointer:\n%s", out)
220	}
221	if contains(out, "manual-c") {
222		t.Errorf("manual skill leaked into system prompt:\n%s", out)
223	}
224}
225
226func TestSystemPromptAddendumEmptyWhenAllSkillsDisableModelInvocation(t *testing.T) {
227	out := SystemPromptAddendum([]*Skill{{Name: "manual", DisableModelInvocation: true}})
228	if out != "" {
229		t.Fatalf("SystemPromptAddendum() = %q, want empty", out)
230	}
231}
232
233func contains(s, sub string) bool {
234	return len(s) >= len(sub) && (s == sub || (len(sub) > 0 && stringIndex(s, sub) >= 0))
235}
236
237func stringIndex(s, sub string) int {
238	for i := 0; i+len(sub) <= len(s); i++ {
239		if s[i:i+len(sub)] == sub {
240			return i
241		}
242	}
243	return -1
244}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close