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("ZOT_AGENT_SKILLS", "")
54 tmp := t.TempDir()
55 zotHome := 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, ".zot", "skills"), "shared", "project version")
67 mk(filepath.Join(zotHome, "skills"), "shared", "global version")
68 // Unique skill in global only.
69 mk(filepath.Join(zotHome, "skills"), "global-only", "from global")
70
71 skills, errs := Discover(zotHome, 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-zot-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 TestVisibleSkillsHidesBuiltins(t *testing.T) {
98 in := []*Skill{
99 {Name: "user-one"},
100 {Name: "built-one", Builtin: true},
101 {Name: "user-two"},
102 }
103 out := VisibleSkills(in)
104 if len(out) != 2 {
105 t.Fatalf("expected 2 visible skills, got %d (%v)", len(out), out)
106 }
107 for _, s := range out {
108 if s.Builtin {
109 t.Errorf("built-in %q leaked into visible set", s.Name)
110 }
111 }
112}
113
114func TestSystemPromptAddendum(t *testing.T) {
115 skills := []*Skill{
116 {Name: "built-a", Description: "Do A.", Builtin: true},
117 {Name: "user-b", Description: "Do B.", Path: "/tmp/skills/user-b/SKILL.md"},
118 {Name: "manual-c", Description: "Do C.", DisableModelInvocation: true},
119 }
120 out := SystemPromptAddendum(skills)
121 if !contains(out, "- built-a [builtin]: Do A.\n") {
122 t.Errorf("builtin entry missing or wrong:\n%s", out)
123 }
124 if !contains(out, "- user-b [/tmp/skills/user-b/SKILL.md]: Do B.\n") {
125 t.Errorf("user entry missing path pointer:\n%s", out)
126 }
127 if contains(out, "manual-c") {
128 t.Errorf("manual skill leaked into system prompt:\n%s", out)
129 }
130}
131
132func TestSystemPromptAddendumEmptyWhenAllSkillsDisableModelInvocation(t *testing.T) {
133 out := SystemPromptAddendum([]*Skill{{Name: "manual", DisableModelInvocation: true}})
134 if out != "" {
135 t.Fatalf("SystemPromptAddendum() = %q, want empty", out)
136 }
137}
138
139func contains(s, sub string) bool {
140 return len(s) >= len(sub) && (s == sub || (len(sub) > 0 && stringIndex(s, sub) >= 0))
141}
142
143func stringIndex(s, sub string) int {
144 for i := 0; i+len(sub) <= len(s); i++ {
145 if s[i:i+len(sub)] == sub {
146 return i
147 }
148 }
149 return -1
150}