package skills_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"rickub.com/bots-garden/ori/internal/skills"
)
// writeSkill creates /.claude/skills/
/SKILL.md with content.
func writeSkill(t *testing.T, base, dir, content string) string {
t.Helper()
full := filepath.Join(base, ".claude", "skills", dir)
if err := os.MkdirAll(full, 0o755); err != nil {
t.Fatal(err)
}
path := filepath.Join(full, "SKILL.md")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func TestDiscoverParsesFrontmatterAndFallsBackToDirectoryName(t *testing.T) {
cwd := t.TempDir()
writeSkill(t, cwd, "quality", "---\nname: quality\ndescription: Audit code quality with qlty.\n---\n# Quality\n")
writeSkill(t, cwd, "bare-dir", "# No frontmatter here\n")
writeSkill(t, cwd, "quoted", "---\nname: \"fancy-name\"\ndescription: 'Single quoted: with colon'\nother: ignored\n---\n")
writeSkill(t, cwd, "folded", "---\nname: folded\ndescription: >\n First line of the\n description.\n---\n")
// A stray file (not a directory) and a directory without SKILL.md are ignored.
if err := os.WriteFile(filepath.Join(cwd, ".claude", "skills", "README.md"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(cwd, ".claude", "skills", "empty"), 0o755); err != nil {
t.Fatal(err)
}
got := skills.New(cwd, "").Discover()
want := []skills.Skill{
{Name: "bare-dir", Description: ""},
{Name: "fancy-name", Description: "Single quoted: with colon"},
{Name: "folded", Description: "First line of the description."},
{Name: "quality", Description: "Audit code quality with qlty."},
}
if len(got) != len(want) {
t.Fatalf("Discover = %+v, want %d skills", got, len(want))
}
for i := range want {
if got[i].Name != want[i].Name || got[i].Description != want[i].Description {
t.Errorf("skill[%d] = %+v, want %+v", i, got[i], want[i])
}
if got[i].Source != skills.SourceProject || filepath.Base(got[i].Path) != "SKILL.md" {
t.Errorf("skill[%d] source/path = %q %q", i, got[i].Source, got[i].Path)
}
}
}
func TestDiscoverMergesUserSkillsAndProjectShadowsThem(t *testing.T) {
cwd, home := t.TempDir(), t.TempDir()
writeSkill(t, home, "shared", "---\nname: shared\ndescription: user version\n---\n")
writeSkill(t, home, "user-only", "---\nname: user-only\ndescription: only at home\n---\n")
writeSkill(t, cwd, "shared", "---\nname: shared\ndescription: project version\n---\n")
got := skills.New(cwd, home).Discover()
if len(got) != 2 {
t.Fatalf("Discover = %+v, want 2 skills", got)
}
if got[0].Name != "shared" || got[0].Description != "project version" || got[0].Source != skills.SourceProject {
t.Errorf("shared = %+v, want the project version", got[0])
}
if got[1].Name != "user-only" || got[1].Source != skills.SourceUser {
t.Errorf("user-only = %+v, want the user skill", got[1])
}
}
func TestDiscoverWithoutSkillDirectoriesIsEmpty(t *testing.T) {
if got := skills.New(t.TempDir(), t.TempDir()).Discover(); len(got) != 0 {
t.Errorf("Discover = %+v, want none", got)
}
}
func TestEndpointReturnsJSONArray(t *testing.T) {
cwd := t.TempDir()
writeSkill(t, cwd, "demo", "---\nname: demo\ndescription: A demo skill\n---\n")
mux := http.NewServeMux()
for pattern, handler := range skills.New(cwd, "").Routes() {
mux.Handle(pattern, handler)
}
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
if rec.Code != http.StatusOK {
t.Fatalf("GET /api/skills = %d", rec.Code)
}
var payload struct {
Skills []skills.Skill `json:"skills"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("not JSON: %v", err)
}
if len(payload.Skills) != 1 || payload.Skills[0].Name != "demo" || payload.Skills[0].Description != "A demo skill" {
t.Errorf("skills = %+v", payload.Skills)
}
// No skills anywhere: still a JSON array, never null.
empty := http.NewServeMux()
for pattern, handler := range skills.New(t.TempDir(), "").Routes() {
empty.Handle(pattern, handler)
}
rec = httptest.NewRecorder()
empty.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/skills", nil))
if body := rec.Body.String(); body != "{\"skills\":[]}\n" {
t.Errorf("empty body = %q", body)
}
}