@@ -0,0 +1,249 @@
9+ "github.com/stretchr/testify/require"
12+func TestParse(t *testing.T) {
22+ wantMeta map[string]string
31+description: Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs.
33+compatibility: Requires python 3.8+, pdfplumber, pdfrw libraries
41+## When to use this skill
42+Use this skill when the user needs to work with PDF files.
44+ wantName: "pdf-processing",
45+ wantDesc: "Extracts text and tables from PDF files, fills PDF forms, and merges multiple PDFs.",
46+ wantLicense: "Apache-2.0",
47+ wantCompat: "Requires python 3.8+, pdfplumber, pdfrw libraries",
48+ wantMeta: map[string]string{"author": "example-org", "version": "1.0"},
49+ wantInstr: "# PDF Processing\n\n## When to use this skill\nUse this skill when the user needs to work with PDF files.",
52+ name: "minimal skill",
55+description: A simple skill for testing.
62+ wantName: "my-skill",
63+ wantDesc: "A simple skill for testing.",
64+ wantInstr: "# My Skill\n\nInstructions here.",
67+ name: "no frontmatter",
68+ content: "# Just Markdown\n\nNo frontmatter here.",
73+ for _, tt := range tests {
74+ t.Run(tt.name, func(t *testing.T) {
77+ // Write content to temp file.
79+ path := filepath.Join(dir, "SKILL.md")
80+ require.NoError(t, os.WriteFile(path, []byte(tt.content), 0o644))
82+ skill, err := Parse(path)
84+ require.Error(t, err)
87+ require.NoError(t, err)
89+ require.Equal(t, tt.wantName, skill.Name)
90+ require.Equal(t, tt.wantDesc, skill.Description)
91+ require.Equal(t, tt.wantLicense, skill.License)
92+ require.Equal(t, tt.wantCompat, skill.Compatibility)
94+ if tt.wantMeta != nil {
95+ require.Equal(t, tt.wantMeta, skill.Metadata)
98+ require.Equal(t, tt.wantInstr, skill.Instructions)
103+func TestSkillValidate(t *testing.T) {
113+ name: "valid skill",
115+ Name: "pdf-processing",
116+ Description: "Processes PDF files.",
117+ Path: "/skills/pdf-processing",
121+ name: "missing name",
122+ skill: Skill{Description: "Some description."},
124+ errMsg: "name is required",
127+ name: "missing description",
128+ skill: Skill{Name: "my-skill", Path: "/skills/my-skill"},
130+ errMsg: "description is required",
133+ name: "name too long",
134+ skill: Skill{Name: strings.Repeat("a", 65), Description: "Some description."},
139+ name: "valid name - mixed case",
140+ skill: Skill{Name: "MySkill", Description: "Some description.", Path: "/skills/MySkill"},
144+ name: "invalid name - starts with hyphen",
145+ skill: Skill{Name: "-my-skill", Description: "Some description."},
147+ errMsg: "alphanumeric with hyphens",
150+ name: "name doesn't match directory",
151+ skill: Skill{Name: "my-skill", Description: "Some description.", Path: "/skills/other-skill"},
153+ errMsg: "must match directory",
156+ name: "description too long",
157+ skill: Skill{Name: "my-skill", Description: strings.Repeat("a", 1025), Path: "/skills/my-skill"},
159+ errMsg: "description exceeds",
162+ name: "compatibility too long",
163+ skill: Skill{Name: "my-skill", Description: "desc", Compatibility: strings.Repeat("a", 501), Path: "/skills/my-skill"},
165+ errMsg: "compatibility exceeds",
169+ for _, tt := range tests {
170+ t.Run(tt.name, func(t *testing.T) {
173+ err := tt.skill.Validate()
175+ require.Error(t, err)
176+ require.Contains(t, err.Error(), tt.errMsg)
178+ require.NoError(t, err)
184+func TestDiscover(t *testing.T) {
187+ tmpDir := t.TempDir()
189+ // Create valid skill 1.
190+ skill1Dir := filepath.Join(tmpDir, "skill-one")
191+ require.NoError(t, os.MkdirAll(skill1Dir, 0o755))
192+ require.NoError(t, os.WriteFile(filepath.Join(skill1Dir, "SKILL.md"), []byte(`---
194+description: First test skill.
199+ // Create valid skill 2 in nested directory.
200+ skill2Dir := filepath.Join(tmpDir, "nested", "skill-two")
201+ require.NoError(t, os.MkdirAll(skill2Dir, 0o755))
202+ require.NoError(t, os.WriteFile(filepath.Join(skill2Dir, "SKILL.md"), []byte(`---
204+description: Second test skill.
209+ // Create invalid skill (won't be included).
210+ invalidDir := filepath.Join(tmpDir, "invalid-dir")
211+ require.NoError(t, os.MkdirAll(invalidDir, 0o755))
212+ require.NoError(t, os.WriteFile(filepath.Join(invalidDir, "SKILL.md"), []byte(`---
214+description: Name doesn't match directory.
218+ skills := Discover([]string{tmpDir})
219+ require.Len(t, skills, 2)
221+ names := make(map[string]bool)
222+ for _, s := range skills {
223+ names[s.Name] = true
225+ require.True(t, names["skill-one"])
226+ require.True(t, names["skill-two"])
229+func TestToPromptXML(t *testing.T) {
233+ {Name: "pdf-processing", Description: "Extracts text from PDFs.", SkillFilePath: "/skills/pdf-processing/SKILL.md"},
234+ {Name: "data-analysis", Description: "Analyzes datasets & charts.", SkillFilePath: "/skills/data-analysis/SKILL.md"},
237+ xml := ToPromptXML(skills)
239+ require.Contains(t, xml, "<available_skills>")
240+ require.Contains(t, xml, "<name>pdf-processing</name>")
241+ require.Contains(t, xml, "<description>Extracts text from PDFs.</description>")
242+ require.Contains(t, xml, "&") // XML escaping
245+func TestToPromptXMLEmpty(t *testing.T) {
247+ require.Empty(t, ToPromptXML(nil))
248+ require.Empty(t, ToPromptXML([]*Skill{}))