Files
multica/server/internal/handler/skill_search_test.go
Naiyuan Qing dd4d58f20e feat: add skill search CLI (#3601)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 15:19:42 +08:00

142 lines
4.3 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSearchSkillsReturnsNormalizedClawHubCandidates(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/search":
if got := r.URL.Query().Get("q"); got != "react" {
t.Fatalf("expected q=react, got %q", got)
}
writeJSON(w, http.StatusOK, map[string]any{
"results": []map[string]any{
{
"slug": "react",
"displayName": "React",
"summary": "React engineering skill",
"ownerHandle": "ivangdavila",
},
{
"slug": "react-expert",
"displayName": "React Expert",
"summary": "Advanced React review",
"ownerHandle": "veeramanikandanr48",
},
},
})
case "/skills/react":
writeJSON(w, http.StatusOK, map[string]any{
"skill": map[string]any{
"slug": "react",
"displayName": "React",
"summary": "React engineering skill",
"stats": map[string]any{
"installsAllTime": 62,
"stars": 3,
},
},
})
case "/skills/react-expert":
writeJSON(w, http.StatusOK, map[string]any{
"skill": map[string]any{
"slug": "react-expert",
"displayName": "React Expert",
"summary": "Advanced React review",
"stats": map[string]any{
"installsAllTime": 11,
"stars": 7,
},
},
})
default:
http.NotFound(w, r)
}
}))
defer upstream.Close()
oldBase := clawHubAPIBase
clawHubAPIBase = upstream.URL
t.Cleanup(func() { clawHubAPIBase = oldBase })
w := httptest.NewRecorder()
req := newRequest(http.MethodGet, "/api/skills/search?q=react", nil)
testHandler.SearchSkills(w, req)
if w.Code != http.StatusOK {
t.Fatalf("SearchSkills: expected 200, got %d: %s", w.Code, w.Body.String())
}
var got []map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("SearchSkills: decode response: %v", err)
}
if len(got) != 2 {
t.Fatalf("expected 2 candidates, got %d: %#v", len(got), got)
}
first := got[0]
if first["name"] != "React" {
t.Fatalf("expected normalized name, got %#v", first["name"])
}
if first["url"] != "https://clawhub.ai/ivangdavila/react" {
t.Fatalf("expected importable ClawHub URL, got %#v", first["url"])
}
if first["source"] != "clawhub.ai" {
t.Fatalf("expected source clawhub.ai, got %#v", first["source"])
}
if first["repo"] != nil {
t.Fatalf("repo should be null when ClawHub has no GitHub repo field, got %#v", first["repo"])
}
if first["github_stars"] != nil {
t.Fatalf("github_stars should not use ClawHub stars, got %#v", first["github_stars"])
}
if first["install_count"] != float64(62) {
t.Fatalf("expected install_count from details stats, got %#v", first["install_count"])
}
if first["description"] != "React engineering skill" {
t.Fatalf("expected description from summary, got %#v", first["description"])
}
}
func TestSearchSkillsEmptyQueryReturns400(t *testing.T) {
w := httptest.NewRecorder()
req := newRequest(http.MethodGet, "/api/skills/search?q=", nil)
testHandler.SearchSkills(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("SearchSkills empty query: expected 400, got %d: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "query is required") {
t.Fatalf("expected query is required error, got %s", w.Body.String())
}
}
func TestSearchSkillsUpstreamUnavailableReturnsStructuredError(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "temporary outage", http.StatusBadGateway)
}))
defer upstream.Close()
oldBase := clawHubAPIBase
clawHubAPIBase = upstream.URL
t.Cleanup(func() { clawHubAPIBase = oldBase })
w := httptest.NewRecorder()
req := newRequest(http.MethodGet, "/api/skills/search?q=react", nil)
testHandler.SearchSkills(w, req)
if w.Code != http.StatusBadGateway {
t.Fatalf("SearchSkills outage: expected 502, got %d: %s", w.Code, w.Body.String())
}
var got map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("decode error body: %v", err)
}
if got["code"] != "upstream_unavailable" {
t.Fatalf("expected structured upstream_unavailable code, got %#v", got)
}
}