mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
Closes the functional gap the reporter hit on alchaincyf/huashu-design (skills.sh/alchaincyf/huashu-design/huashu-design) without expanding candidatePaths unconditionally, which would let an unrelated root SKILL.md hijack a different skill URL in a multi-skill repo. Try SKILL.md at the repo root before falling into the recursive tree fallback added in #1432. Verify the frontmatter name matches the requested skill so only genuine single-skill repos take the fast path. For those repos this also shaves the recursive tree API call. Also clarifies the candidate-path comment so the root case is explicit.
623 lines
20 KiB
Go
623 lines
20 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFetchFromSkillsSh_UsesEntryURLForNestedDirectories(t *testing.T) {
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/acme/skills":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/acme/skills/contents/skills/pptx":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("top-level ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "editing.md",
|
|
Path: "skills/pptx/editing.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/skills/main/skills/pptx/editing.md",
|
|
},
|
|
{
|
|
Name: "scripts",
|
|
Path: "skills/pptx/scripts",
|
|
Type: "dir",
|
|
URL: "https://api.github.com/repos/acme/skills/contents/skills/pptx/scripts?ref=main",
|
|
},
|
|
})
|
|
case "/repos/acme/skills/contents/skills/pptx/scripts":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("scripts ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "add_slide.py",
|
|
Path: "skills/pptx/scripts/add_slide.py",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/skills/main/skills/pptx/scripts/add_slide.py",
|
|
},
|
|
{
|
|
Name: "office",
|
|
Path: "skills/pptx/scripts/office",
|
|
Type: "dir",
|
|
URL: "https://api.github.com/repos/acme/skills/contents/skills/pptx/scripts/office?ref=main",
|
|
},
|
|
})
|
|
case "/repos/acme/skills/contents/skills/pptx/scripts/office":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("office ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "foo.py",
|
|
Path: "skills/pptx/scripts/office/foo.py",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/skills/main/skills/pptx/scripts/office/foo.py",
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/acme/skills/main/skills/pptx/SKILL.md":
|
|
w.Write([]byte("---\nname: PPTX\n---\ncontent"))
|
|
case "/acme/skills/main/skills/pptx/editing.md":
|
|
w.Write([]byte("editing"))
|
|
case "/acme/skills/main/skills/pptx/scripts/add_slide.py":
|
|
w.Write([]byte("print('slide')"))
|
|
case "/acme/skills/main/skills/pptx/scripts/office/foo.py":
|
|
w.Write([]byte("print('office')"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/acme/skills/pptx")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
|
|
gotPaths := importedFilePaths(result.files)
|
|
wantPaths := []string{"editing.md", "scripts/add_slide.py", "scripts/office/foo.py"}
|
|
if !equalStrings(gotPaths, wantPaths) {
|
|
t.Fatalf("files = %v, want %v", gotPaths, wantPaths)
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/acme/skills/contents/skills/pptx/scripts?ref=main") {
|
|
t.Fatalf("expected scripts directory to be fetched via entry.URL, got requests %v", *requests)
|
|
}
|
|
if containsString(*requests, "api.github.com /repos/acme/skills/contents/skills/pptx?ref=main/scripts") {
|
|
t.Fatalf("saw buggy query-appended request: %v", *requests)
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_FallbackDoesNotDoubleEscapeDirectoryNames(t *testing.T) {
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/acme/skills":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/acme/skills/contents/skills/pptx":
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "my dir",
|
|
Path: "skills/pptx/my dir",
|
|
Type: "dir",
|
|
},
|
|
})
|
|
case "/repos/acme/skills/contents/skills/pptx/my dir":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("fallback ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "note.md",
|
|
Path: "skills/pptx/my dir/note.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/skills/main/skills/pptx/my%20dir/note.md",
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/acme/skills/main/skills/pptx/SKILL.md":
|
|
w.Write([]byte("---\nname: PPTX\n---\ncontent"))
|
|
case "/acme/skills/main/skills/pptx/my dir/note.md":
|
|
w.Write([]byte("note"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/acme/skills/pptx")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
|
|
gotPaths := importedFilePaths(result.files)
|
|
wantPaths := []string{"my dir/note.md"}
|
|
if !equalStrings(gotPaths, wantPaths) {
|
|
t.Fatalf("files = %v, want %v", gotPaths, wantPaths)
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/acme/skills/contents/skills/pptx/my%20dir?ref=main") {
|
|
t.Fatalf("expected fallback request with single escaping, got %v", *requests)
|
|
}
|
|
for _, request := range *requests {
|
|
if strings.Contains(request, "%2520") {
|
|
t.Fatalf("unexpected double-escaped request: %v", *requests)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_LogsSubdirectoryFailures(t *testing.T) {
|
|
client, _ := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/acme/skills":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/acme/skills/contents/skills/pptx":
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "scripts",
|
|
Path: "skills/pptx/scripts",
|
|
Type: "dir",
|
|
URL: "https://api.github.com/repos/acme/skills/contents/skills/pptx/scripts?ref=main",
|
|
},
|
|
})
|
|
case "/repos/acme/skills/contents/skills/pptx/scripts":
|
|
http.Error(w, "missing", http.StatusNotFound)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/acme/skills/main/skills/pptx/SKILL.md":
|
|
w.Write([]byte("---\nname: PPTX\n---\ncontent"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
var logs bytes.Buffer
|
|
prev := slog.Default()
|
|
slog.SetDefault(slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelInfo})))
|
|
t.Cleanup(func() {
|
|
slog.SetDefault(prev)
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/acme/skills/pptx")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
if len(result.files) != 0 {
|
|
t.Fatalf("expected no files when subdirectory listing fails, got %v", importedFilePaths(result.files))
|
|
}
|
|
|
|
logOutput := logs.String()
|
|
if !strings.Contains(logOutput, "skills.sh import: failed to list subdirectory") {
|
|
t.Fatalf("expected warning log, got %q", logOutput)
|
|
}
|
|
if !strings.Contains(logOutput, "status=404") {
|
|
t.Fatalf("expected status in warning log, got %q", logOutput)
|
|
}
|
|
if !strings.Contains(logOutput, "skills/pptx/scripts?ref=main") {
|
|
t.Fatalf("expected subdirectory URL in warning log, got %q", logOutput)
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_ResolvesAliasedSkillNamesViaFrontmatter(t *testing.T) {
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/vercel-labs/agent-skills":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/vercel-labs/agent-skills/git/trees/main":
|
|
if got := r.URL.Query().Get("recursive"); got != "1" {
|
|
t.Fatalf("tree recursive = %q, want 1", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, githubTreeResponse{
|
|
Tree: []githubTreeEntry{
|
|
{Path: "skills/composition-patterns/SKILL.md", Type: "blob"},
|
|
{Path: "skills/react-best-practices/SKILL.md", Type: "blob"},
|
|
},
|
|
})
|
|
case "/repos/vercel-labs/agent-skills/contents/skills/composition-patterns":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("resolved dir ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "rules.md",
|
|
Path: "skills/composition-patterns/rules.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/composition-patterns/rules.md",
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/vercel-labs/agent-skills/main/skills/composition-patterns/SKILL.md":
|
|
w.Write([]byte("---\nname: vercel-composition-patterns\ndescription: aliased skill\n---\ncontent"))
|
|
case "/vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md":
|
|
w.Write([]byte("---\nname: vercel-react-best-practices\n---\ncontent"))
|
|
case "/vercel-labs/agent-skills/main/skills/composition-patterns/rules.md":
|
|
w.Write([]byte("rules"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/vercel-labs/agent-skills/vercel-composition-patterns")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
|
|
if result.name != "vercel-composition-patterns" {
|
|
t.Fatalf("name = %q, want vercel-composition-patterns", result.name)
|
|
}
|
|
gotPaths := importedFilePaths(result.files)
|
|
wantPaths := []string{"rules.md"}
|
|
if !equalStrings(gotPaths, wantPaths) {
|
|
t.Fatalf("files = %v, want %v", gotPaths, wantPaths)
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/vercel-labs/agent-skills/git/trees/main?recursive=1") {
|
|
t.Fatalf("expected fallback tree lookup, got requests %v", *requests)
|
|
}
|
|
for _, request := range *requests {
|
|
if request == "raw.githubusercontent.com /vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md" {
|
|
t.Fatalf("unexpected non-matching fallback fetch: %v", *requests)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_ResolvesRootLevelSkillMd(t *testing.T) {
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/alchaincyf/huashu-design":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "master"})
|
|
case "/repos/alchaincyf/huashu-design/git/trees/master":
|
|
if got := r.URL.Query().Get("recursive"); got != "1" {
|
|
t.Fatalf("tree recursive = %q, want 1", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, githubTreeResponse{
|
|
Tree: []githubTreeEntry{
|
|
{Path: "README.md", Type: "blob"},
|
|
{Path: "SKILL.md", Type: "blob"},
|
|
{Path: "assets", Type: "tree"},
|
|
{Path: "assets/logo.png", Type: "blob"},
|
|
},
|
|
})
|
|
case "/repos/alchaincyf/huashu-design/contents":
|
|
if got := r.URL.Query().Get("ref"); got != "master" {
|
|
t.Fatalf("root contents ref = %q, want master", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "README.md",
|
|
Path: "README.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/alchaincyf/huashu-design/master/README.md",
|
|
},
|
|
{
|
|
Name: "SKILL.md",
|
|
Path: "SKILL.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/alchaincyf/huashu-design/master/SKILL.md",
|
|
},
|
|
{
|
|
Name: "assets",
|
|
Path: "assets",
|
|
Type: "dir",
|
|
URL: "https://api.github.com/repos/alchaincyf/huashu-design/contents/assets?ref=master",
|
|
},
|
|
})
|
|
case "/repos/alchaincyf/huashu-design/contents/assets":
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "logo.png",
|
|
Path: "assets/logo.png",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/alchaincyf/huashu-design/master/assets/logo.png",
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/alchaincyf/huashu-design/master/SKILL.md":
|
|
w.Write([]byte("---\nname: huashu-design\ndescription: hi-fi HTML prototypes\n---\nbody"))
|
|
case "/alchaincyf/huashu-design/master/README.md":
|
|
w.Write([]byte("# Readme"))
|
|
case "/alchaincyf/huashu-design/master/assets/logo.png":
|
|
w.Write([]byte("PNGBYTES"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/alchaincyf/huashu-design/huashu-design")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
if result.name != "huashu-design" {
|
|
t.Fatalf("name = %q, want huashu-design", result.name)
|
|
}
|
|
if !strings.HasPrefix(result.content, "---\nname: huashu-design") {
|
|
t.Fatalf("SKILL.md content not populated, got %q", result.content)
|
|
}
|
|
gotPaths := importedFilePaths(result.files)
|
|
wantPaths := []string{"README.md", "assets/logo.png"}
|
|
if !equalStrings(gotPaths, wantPaths) {
|
|
t.Fatalf("files = %v, want %v", gotPaths, wantPaths)
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/alchaincyf/huashu-design/contents?ref=master") {
|
|
t.Fatalf("expected root contents listing, got %v", *requests)
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_RootSkillMdFastPathSkipsFrontmatterMismatch(t *testing.T) {
|
|
// Multi-skill repo with an unrelated root SKILL.md (skill "other") plus a
|
|
// subdir skill "wanted". URL requests "wanted". The fast-path must reject
|
|
// the root SKILL.md on frontmatter mismatch and fall through to the tree
|
|
// fallback, which then resolves "wanted" correctly.
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/acme/multi":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/acme/multi/git/trees/main":
|
|
writeJSON(w, http.StatusOK, githubTreeResponse{
|
|
Tree: []githubTreeEntry{
|
|
{Path: "SKILL.md", Type: "blob"},
|
|
{Path: "extras/wanted/SKILL.md", Type: "blob"},
|
|
},
|
|
})
|
|
case "/repos/acme/multi/contents/extras/wanted":
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "ref.md",
|
|
Path: "extras/wanted/ref.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/multi/main/extras/wanted/ref.md",
|
|
},
|
|
})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/acme/multi/main/SKILL.md":
|
|
w.Write([]byte("---\nname: other\n---\ncontent"))
|
|
case "/acme/multi/main/extras/wanted/SKILL.md":
|
|
w.Write([]byte("---\nname: wanted\ndescription: the right one\n---\ncontent"))
|
|
case "/acme/multi/main/extras/wanted/ref.md":
|
|
w.Write([]byte("ref"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
result, err := fetchFromSkillsSh(client, "https://skills.sh/acme/multi/wanted")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
if result.name != "wanted" {
|
|
t.Fatalf("name = %q, want wanted (root SKILL.md must not hijack the mismatched request)", result.name)
|
|
}
|
|
gotPaths := importedFilePaths(result.files)
|
|
wantPaths := []string{"ref.md"}
|
|
if !equalStrings(gotPaths, wantPaths) {
|
|
t.Fatalf("files = %v, want %v", gotPaths, wantPaths)
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/acme/multi/git/trees/main?recursive=1") {
|
|
t.Fatalf("expected tree fallback to run after fast-path frontmatter miss, got %v", *requests)
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_ReturnsActionableErrorForTruncatedTrees(t *testing.T) {
|
|
client, requests := newGitHubFixtureClient(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Header.Get("X-Test-Original-Host") {
|
|
case "api.github.com":
|
|
switch r.URL.Path {
|
|
case "/repos/acme/skills":
|
|
writeJSON(w, http.StatusOK, map[string]any{"default_branch": "main"})
|
|
case "/repos/acme/skills/git/trees/main":
|
|
if got := r.URL.Query().Get("recursive"); got != "1" {
|
|
t.Fatalf("tree recursive = %q, want 1", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, githubTreeResponse{
|
|
Tree: []githubTreeEntry{
|
|
{Path: "skills/deploy-to-vercel/SKILL.md", Type: "blob"},
|
|
},
|
|
Truncated: true,
|
|
})
|
|
case "/repos/acme/skills/contents/skills":
|
|
if got := r.URL.Query().Get("ref"); got != "main" {
|
|
t.Fatalf("skills ref = %q, want main", got)
|
|
}
|
|
writeJSON(w, http.StatusOK, []githubContentEntry{
|
|
{
|
|
Name: "SKILL.md",
|
|
Path: "skills/deploy-to-vercel/SKILL.md",
|
|
Type: "file",
|
|
DownloadURL: "https://raw.githubusercontent.com/acme/skills/main/skills/deploy-to-vercel/SKILL.md",
|
|
},
|
|
})
|
|
case "/repos/acme/skills/contents/.claude/skills":
|
|
http.NotFound(w, r)
|
|
case "/repos/acme/skills/contents/plugin/skills":
|
|
http.NotFound(w, r)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
case "raw.githubusercontent.com":
|
|
switch r.URL.Path {
|
|
case "/acme/skills/main/skills/deploy-to-vercel/SKILL.md":
|
|
w.Write([]byte("---\nname: deploy-to-vercel\n---\ncontent"))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
_, err := fetchFromSkillsSh(client, "https://skills.sh/acme/skills/vercel-composition-patterns")
|
|
if err == nil {
|
|
t.Fatal("expected error for truncated tree fallback miss")
|
|
}
|
|
if !strings.Contains(err.Error(), "tree is too large to scan exhaustively") {
|
|
t.Fatalf("error = %q, want actionable truncated-tree message", err.Error())
|
|
}
|
|
if !containsString(*requests, "api.github.com /repos/acme/skills/contents/skills?ref=main") {
|
|
t.Fatalf("expected conventional prefix listing, got %v", *requests)
|
|
}
|
|
}
|
|
|
|
func TestFetchFromSkillsSh_AnthropicPptxIntegration(t *testing.T) {
|
|
if os.Getenv("MULTICA_RUN_SKILLS_SH_INTEGRATION") == "" {
|
|
t.Skip("set MULTICA_RUN_SKILLS_SH_INTEGRATION=1 to run live GitHub integration test")
|
|
}
|
|
|
|
result, err := fetchFromSkillsSh(&http.Client{Timeout: 30 * time.Second}, "https://skills.sh/anthropics/skills/pptx")
|
|
if err != nil {
|
|
t.Fatalf("fetchFromSkillsSh: %v", err)
|
|
}
|
|
|
|
gotPaths := importedFilePaths(result.files)
|
|
for _, want := range []string{
|
|
"scripts/__init__.py",
|
|
"scripts/add_slide.py",
|
|
"scripts/clean.py",
|
|
"scripts/thumbnail.py",
|
|
} {
|
|
if !containsString(gotPaths, want) {
|
|
t.Fatalf("missing %q in %v", want, gotPaths)
|
|
}
|
|
}
|
|
}
|
|
|
|
type rewriteGitHubTransport struct {
|
|
target *url.URL
|
|
base http.RoundTripper
|
|
hosts map[string]struct{}
|
|
}
|
|
|
|
func (t *rewriteGitHubTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
clone := req.Clone(req.Context())
|
|
if _, ok := t.hosts[clone.URL.Host]; ok {
|
|
headers := clone.Header.Clone()
|
|
headers.Set("X-Test-Original-Host", req.URL.Host)
|
|
clone.Header = headers
|
|
clone.URL.Scheme = t.target.Scheme
|
|
clone.URL.Host = t.target.Host
|
|
clone.Host = t.target.Host
|
|
}
|
|
return t.base.RoundTrip(clone)
|
|
}
|
|
|
|
func newGitHubFixtureClient(t *testing.T, handler http.HandlerFunc) (*http.Client, *[]string) {
|
|
t.Helper()
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
requests []string
|
|
)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
requests = append(requests, r.Header.Get("X-Test-Original-Host")+" "+r.URL.RequestURI())
|
|
mu.Unlock()
|
|
handler(w, r)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
target, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("parse server url: %v", err)
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: &rewriteGitHubTransport{
|
|
target: target,
|
|
base: http.DefaultTransport,
|
|
hosts: map[string]struct{}{
|
|
"api.github.com": {},
|
|
"raw.githubusercontent.com": {},
|
|
},
|
|
},
|
|
}, &requests
|
|
}
|
|
|
|
func importedFilePaths(files []importedFile) []string {
|
|
paths := make([]string, 0, len(files))
|
|
for _, file := range files {
|
|
paths = append(paths, file.path)
|
|
}
|
|
sort.Strings(paths)
|
|
return paths
|
|
}
|
|
|
|
func equalStrings(got, want []string) bool {
|
|
if len(got) != len(want) {
|
|
return false
|
|
}
|
|
for i := range got {
|
|
if got[i] != want[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func containsString(values []string, want string) bool {
|
|
for _, value := range values {
|
|
if value == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|