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) } }